Compare commits
330 Commits
1.0.0
...
v2025.3.26
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a728672c8 | ||
|
|
c8178a6c5f | ||
|
|
9d546fd214 | ||
|
|
d467adbdec | ||
|
|
c08776d028 | ||
|
|
c3c770b2ed | ||
|
|
63a05954f8 | ||
|
|
98c81107fc | ||
|
|
fb862564e1 | ||
|
|
c0bad34e36 | ||
|
|
f7a2681157 | ||
|
|
ee5c47f2dc | ||
|
|
c28151320c | ||
|
|
e5c4076278 | ||
|
|
8673796919 | ||
|
|
b4c513a585 | ||
|
|
f48aa837a9 | ||
|
|
e347f6080c | ||
|
|
b371555d37 | ||
|
|
4c3fa36d4f | ||
|
|
1d4ede336c | ||
|
|
740f8ef022 | ||
|
|
646420d672 | ||
|
|
55f7f246b0 | ||
|
|
0b4c9f9ae2 | ||
|
|
f6eaa11d65 | ||
|
|
11f0b66360 | ||
|
|
f0e5dbe278 | ||
|
|
e260e3fc71 | ||
|
|
c64f865216 | ||
|
|
3217338966 | ||
|
|
5d6ecdc21b | ||
|
|
6beaf9007c | ||
|
|
1925ffda31 | ||
|
|
217c4975c4 | ||
|
|
4dd474c0fb | ||
|
|
78150bcecd | ||
|
|
2e7c9514b4 | ||
|
|
ba862ff586 | ||
|
|
7d58082525 | ||
|
|
4f96b0a784 | ||
|
|
db43da6577 | ||
|
|
9a6e210bae | ||
|
|
1b31ff04df | ||
|
|
ebdd0d701e | ||
|
|
102d6bbcdb | ||
|
|
74746fc2c2 | ||
|
|
6f6884c18a | ||
|
|
ec7534ff2c | ||
|
|
db270779e6 | ||
|
|
09ae4c542b | ||
|
|
5b1a9c6d4d | ||
|
|
1c0596587f | ||
|
|
8e7b7bd4e1 | ||
|
|
48c5f5cc4c | ||
|
|
7417caa778 | ||
|
|
0864806770 | ||
|
|
adcde5efcc | ||
|
|
ce91b2e532 | ||
|
|
826a29cd8c | ||
|
|
b2b0300aa1 | ||
|
|
dbc25ca582 | ||
|
|
40a4e58276 | ||
|
|
2c2d689f53 | ||
|
|
fdca30ce3a | ||
|
|
7b3bad4102 | ||
|
|
531b01bca3 | ||
|
|
645c6979a4 | ||
|
|
5c94b40e4d | ||
|
|
83603a12a7 | ||
|
|
2aba86e424 | ||
|
|
8a7e0140eb | ||
|
|
797a35eaa5 | ||
|
|
1763435aa1 | ||
|
|
7952c1fceb | ||
|
|
fbb8b00315 | ||
|
|
2bf7d1e31f | ||
|
|
cb2bc61c6f | ||
|
|
b3f23fc4db | ||
|
|
67bd9e7996 | ||
|
|
4b9ae00452 | ||
|
|
4baaefc8c5 | ||
|
|
a6f17c632e | ||
|
|
4c249f0806 | ||
|
|
825014e370 | ||
|
|
c91466a023 | ||
|
|
92c61e4c26 | ||
|
|
b34d2d8d76 | ||
|
|
c287a82211 | ||
|
|
1144ac34a7 | ||
|
|
1b66f0c0d8 | ||
|
|
e597d3b484 | ||
|
|
cdc4b43925 | ||
|
|
0ff14fc01c | ||
|
|
5ccbbb6bb5 | ||
|
|
ec4a8659eb | ||
|
|
34ac6755a9 | ||
|
|
e21ba1b800 | ||
|
|
17a234f679 | ||
|
|
d504dc6d13 | ||
|
|
0b749d1699 | ||
|
|
4e9a24c8f2 | ||
|
|
c81b1a730d | ||
|
|
8d3cd7b151 | ||
|
|
5ee1ae4a32 | ||
|
|
dab51f7a70 | ||
|
|
a20d4e721d | ||
|
|
f4da21d645 | ||
|
|
fc37440f6b | ||
|
|
d7b47a7010 | ||
|
|
85d71ae58e | ||
|
|
23dc25f642 | ||
|
|
467bbd8923 | ||
|
|
6be5c0fa05 | ||
|
|
4fac915778 | ||
|
|
d27bcbd334 | ||
|
|
d46872ffbd | ||
|
|
29da37739d | ||
|
|
d7584bc4de | ||
|
|
1f78cc3589 | ||
|
|
a3b718c149 | ||
|
|
37e63538e2 | ||
|
|
70ee9df22a | ||
|
|
b764c978f1 | ||
|
|
fc8dbb919c | ||
|
|
02e3d1df11 | ||
|
|
e074ab2c39 | ||
|
|
e2e5a063e7 | ||
|
|
c4c2bea73d | ||
|
|
bfd9515387 | ||
|
|
7029d75790 | ||
|
|
13d1c75b76 | ||
|
|
aaf53f651a | ||
|
|
dad9ece712 | ||
|
|
6e17e89961 | ||
|
|
fae5a5fb6a | ||
|
|
96f2898111 | ||
|
|
e24965393b | ||
|
|
7e5d135483 | ||
|
|
c8827da35a | ||
|
|
268bc8b1f6 | ||
|
|
2869b37053 | ||
|
|
b237341fda | ||
|
|
957de8ad8b | ||
|
|
e622b7d86e | ||
|
|
95f9f1840f | ||
|
|
267f6f638f | ||
|
|
b459abb35d | ||
|
|
d79bdc8bc1 | ||
|
|
863e88c579 | ||
|
|
853f6b180e | ||
|
|
b4c55ce233 | ||
|
|
22111411c3 | ||
|
|
ddfc7c1216 | ||
|
|
908086c0c0 | ||
|
|
2c0e2ec698 | ||
|
|
cb4690bf88 | ||
|
|
100fb3e2a9 | ||
|
|
1bb13866bc | ||
|
|
05aaf82849 | ||
|
|
255a214554 | ||
|
|
5d0de949a8 | ||
|
|
03229fa46b | ||
|
|
b4cdd2d28b | ||
|
|
ef838af18b | ||
|
|
62defa1ebc | ||
|
|
3dd9790015 | ||
|
|
c25304d28b | ||
|
|
222ba03841 | ||
|
|
3f73a5a521 | ||
|
|
3a3e0b0543 | ||
|
|
0006501cc8 | ||
|
|
c5fbe5fdae | ||
|
|
66d85cf0a2 | ||
|
|
24145894b6 | ||
|
|
75f680a298 | ||
|
|
2658f207dc | ||
|
|
626f99f0d1 | ||
|
|
4d541e81a2 | ||
|
|
6dfe3fd135 | ||
|
|
915e12eab3 | ||
|
|
bcfcbfeef0 | ||
|
|
1dc731de1e | ||
|
|
a580f9254a | ||
|
|
9b080bbb45 | ||
|
|
86183f4585 | ||
|
|
97ab29259a | ||
|
|
91f3e66239 | ||
|
|
713b25d2db | ||
|
|
d0b65e7063 | ||
|
|
f062306158 | ||
|
|
ae7b617e83 | ||
|
|
1035f2a800 | ||
|
|
cb28b18541 | ||
|
|
9d42eb2729 | ||
|
|
7b93d4d8ca | ||
|
|
3e13ef007b | ||
|
|
6ff1b68f1b | ||
|
|
a6547db195 | ||
|
|
567414a136 | ||
|
|
34dc38a95f | ||
|
|
6d2ab3ef41 | ||
|
|
e55506705e | ||
|
|
322e87efbd | ||
|
|
1628381295 | ||
|
|
8afc26badb | ||
|
|
d5db2ef879 | ||
|
|
509cd2dbca | ||
|
|
3de2ad3cdc | ||
|
|
b00bddcdec | ||
|
|
64b37b687c | ||
|
|
e81319bb4f | ||
|
|
7bc219d1a5 | ||
|
|
0f2f58e6b8 | ||
|
|
2dc0b95b45 | ||
|
|
869eced99e | ||
|
|
f5aa70bf61 | ||
|
|
71289d1408 | ||
|
|
f6297d224c | ||
|
|
0bfa50e2b6 | ||
|
|
0d182b923f | ||
|
|
7f19d00a23 | ||
|
|
f514a0083d | ||
|
|
7fbe178c78 | ||
|
|
40fe30ce2f | ||
|
|
1751be729b | ||
|
|
d82ace220a | ||
|
|
9c51ecde2f | ||
|
|
d3cf202c88 | ||
|
|
847cacc71e | ||
|
|
23149b8a28 | ||
|
|
698f496a3c | ||
|
|
8ac97f43ff | ||
|
|
0abf7c9e5a | ||
|
|
a55920f445 | ||
|
|
775635a48c | ||
|
|
5bc7cfab0a | ||
|
|
e3e06d342b | ||
|
|
16e187b96c | ||
|
|
399513cf14 | ||
|
|
3f024faf82 | ||
|
|
dadfe1cf54 | ||
|
|
9cd6761778 | ||
|
|
1d58d6b224 | ||
|
|
db4a2b5fa9 | ||
|
|
af3f2b03dc | ||
|
|
ccbb835c83 | ||
|
|
97b5faee4a | ||
|
|
c9a8192d60 | ||
|
|
1af138312a | ||
|
|
272c990248 | ||
|
|
2907285915 | ||
|
|
9f7b7b8a64 | ||
|
|
02bfe4758e | ||
|
|
6483243d2a | ||
|
|
1ea534b3c0 | ||
|
|
2fcd89ab97 | ||
|
|
b5c44870fe | ||
|
|
54f0a0b585 | ||
|
|
ab6f400930 | ||
|
|
a376d1d92c | ||
|
|
a653ef9fa8 | ||
|
|
ce29514b54 | ||
|
|
1fd149bbd5 | ||
|
|
9dc8fa97df | ||
|
|
7c52cd1d26 | ||
|
|
338ce91ffd | ||
|
|
6e5f57d62e | ||
|
|
b1a0e9575b | ||
|
|
88fb3ce94c | ||
|
|
60d8efc158 | ||
|
|
a41ab5499a | ||
|
|
a54f769ea2 | ||
|
|
9a46788339 | ||
|
|
def92ad722 | ||
|
|
7e27996f17 | ||
|
|
ad428f83f8 | ||
|
|
d3c6c1d570 | ||
|
|
1554d3309d | ||
|
|
e7560f3e9b | ||
|
|
daa29b37a5 | ||
|
|
9a41560bee | ||
|
|
975ad611df | ||
|
|
b764770729 | ||
|
|
88aa793774 | ||
|
|
180bec8866 | ||
|
|
420d5b60f1 | ||
|
|
af1bc685a7 | ||
|
|
b0922b0878 | ||
|
|
200a160acf | ||
|
|
9fae9fc034 | ||
|
|
9a3393bfc3 | ||
|
|
64270d5df2 | ||
|
|
e808ca47b6 | ||
|
|
1b3c043ce6 | ||
|
|
04446d7521 | ||
|
|
2306a8e225 | ||
|
|
46aff404d4 | ||
|
|
1e5d9bc469 | ||
|
|
98844ce717 | ||
|
|
2bd91c6555 | ||
|
|
2166b0a39b | ||
|
|
2f2b19f5d7 | ||
|
|
685a7d23b2 | ||
|
|
5f1eaf02c4 | ||
|
|
116dae19cf | ||
|
|
a35b42f831 | ||
|
|
513cd69e3e | ||
|
|
0ce01bcdf0 | ||
|
|
afe5474264 | ||
|
|
f35847823b | ||
|
|
15120c98da | ||
|
|
6903abd6f8 | ||
|
|
f9a0c8d94e | ||
|
|
0c04272153 | ||
|
|
cf7e8415e6 | ||
|
|
1ab6875790 | ||
|
|
cc40da8371 | ||
|
|
29158f51af | ||
|
|
9665462ae5 | ||
|
|
71e3953cd8 | ||
|
|
bedeaad1f2 | ||
|
|
4f05820e2e | ||
|
|
abf05aabd8 | ||
|
|
9016289e96 | ||
|
|
bab53711b7 | ||
|
|
de67f07f41 | ||
|
|
869223cfb9 | ||
|
|
d240239fcc | ||
|
|
6b62385cad |
54
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Bug 报告
|
||||
|
||||
## 基本信息
|
||||
### Bug 描述
|
||||
<请用简洁明了的语言概括 Bug 的核心问题,例如:“登录页面输入错误密码后无提示信息”>
|
||||
|
||||
### 软件版本信息
|
||||
<说明你所使用的软件版本,在关于界面中可以找到>
|
||||
|
||||
### 运行操作系统和环境
|
||||
- **操作系统**:<例如 Windows 10、macOS 12.6、Ubuntu 22.04 等>
|
||||
- **浏览器(如果是网页应用)**:<如 Chrome 108、Firefox 107 等,同时说明浏览器的版本和是否使用了特殊的插件>
|
||||
- **其他相关环境信息**:<例如运行项目的服务器配置、数据库版本等>
|
||||
|
||||
## Bug 描述
|
||||
### 预期行为
|
||||
<详细描述你认为在正常情况下系统应该呈现的行为。例如:“当用户在登录页面输入错误密码时,页面应弹出提示框显示‘密码错误,请重新输入’”>
|
||||
|
||||
### 实际行为
|
||||
<准确描述实际发生的情况。可以包括错误信息、页面显示异常、功能无法正常使用等具体表现。例如:“当输入错误密码后,页面没有任何提示,也没有重新聚焦到密码输入框,登录按钮依然可点击”>
|
||||
|
||||
### 复现步骤
|
||||
<提供详细的步骤,让开发者能够按照这些步骤重现 Bug。步骤要尽量清晰、具体,例如:
|
||||
1. 打开项目的登录页面(URL:[具体登录页面 URL])。
|
||||
2. 在用户名输入框输入已注册的用户名。
|
||||
3. 在密码输入框输入错误的密码。
|
||||
4. 点击登录按钮。>
|
||||
|
||||
### 频率
|
||||
<说明 Bug 出现的频率,例如“每次都会出现”“偶尔出现(约 10% 的概率)”等>
|
||||
|
||||
## 相关信息
|
||||
### 错误日志
|
||||
<如果有错误日志或控制台输出信息,请提供完整的内容。可以使用代码块来展示,例如:>
|
||||
|
||||
|
||||
|
||||
### 截图或视频
|
||||
<如果 Bug 涉及页面显示问题或操作流程异常,附上相关的截图或录屏视频会非常有帮助。可以直接上传截图文件,或者提供视频的链接>
|
||||
|
||||
### 可能的原因分析(可选)
|
||||
<如果你对 Bug 产生的原因有一些初步的猜测或分析,可以在这里简要说明。这有助于开发者更快地定位问题,但不是必需的>
|
||||
|
||||
## 补充说明
|
||||
<如果有其他与 Bug 相关但不属于上述分类的信息,可以在这里进行补充,例如之前是否进行过特定的配置更改、是否与其他功能存在关联等>
|
||||
10
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
48
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# Pull Request 信息
|
||||
|
||||
## 本次 PR 概述
|
||||
请简要描述这个 Pull Request 做了什么改动。例如:
|
||||
- 修复了某个特定功能的 bug
|
||||
- 实现了一个新的功能特性
|
||||
- 对代码进行了优化,提升了性能
|
||||
|
||||
## 相关问题
|
||||
如果这个 PR 是为了解决某个 Issue,请在此处关联对应的 Issue 编号,格式为 `Fixes #<issue-number>`。例如:
|
||||
Fixes #123
|
||||
|
||||
## 改动内容详细说明
|
||||
### 代码修改
|
||||
- 列出主要修改的文件和修改点。例如:
|
||||
- `app_linux.go`:
|
||||
- 修改了函数 `GetStockList` 的逻辑,从使用 `for` 循环改为 `sum` 函数,提升了计算效率。
|
||||
- `app_test.go`:
|
||||
- 新增了针对 `GetStockList` 函数的单元测试,确保修改后的逻辑正确。
|
||||
|
||||
### 新增功能
|
||||
如果有新增功能,请详细描述该功能的使用方法和特点。例如:
|
||||
- 新增了一个用户认证模块,支持使用用户名和密码进行登录。使用方法如下:
|
||||
- 调用 `authenticate_user(username, password)` 函数进行认证。
|
||||
- 若认证成功,返回 `True`;否则返回 `False`。
|
||||
|
||||
### 删除内容
|
||||
如果有删除的代码或文件,请说明删除的原因。例如:
|
||||
- 删除了 `app_test.go` 文件,因为该模块的功能已经被新的模块替代,不再需要。
|
||||
|
||||
## 测试情况
|
||||
### 单元测试
|
||||
- 列出运行的单元测试以及测试结果。例如:
|
||||
- 运行了 `app_test.go` 进行单元测试,所有测试用例均通过。
|
||||
- 测试覆盖率达到了 90%。
|
||||
|
||||
### 集成测试
|
||||
如果进行了集成测试,请描述测试环境和测试结果。例如:
|
||||
- 在本地开发环境(Wails CLI v2.10.1 node v18.19.1 )中进行了集成测试,功能正常。
|
||||
- 在 CI/CD 环境中也进行了测试,所有步骤均通过。
|
||||
|
||||
## 注意事项
|
||||
- 提醒其他开发者在审查代码时需要注意的地方。例如:
|
||||
- 本次修改涉及到数据库表结构的变更,请确保在部署前进行数据库迁移。
|
||||
- 新增的功能依赖于第三方库 `requests`,请确保在环境中安装该库。
|
||||
|
||||
## 其他补充说明
|
||||
- 可以在这里提供任何其他需要说明的信息,例如设计文档的链接、相关讨论的记录等。
|
||||
18
.github/workflows/main.yml
vendored
@@ -17,9 +17,12 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build:
|
||||
- name: 'go-stock'
|
||||
- name: 'go-stock-windows-amd64.exe'
|
||||
platform: 'windows/amd64'
|
||||
os: 'windows-latest'
|
||||
# - name: 'go-stock-linux-amd64'
|
||||
# platform: 'linux/amd64'
|
||||
# os: 'ubuntu-latest'
|
||||
|
||||
runs-on: ${{ matrix.build.os }}
|
||||
steps:
|
||||
@@ -28,11 +31,20 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Build wails
|
||||
uses: dAppServer/wails-build-action@v2.2
|
||||
- name: Get commit message
|
||||
id: get_commit_message
|
||||
run: |
|
||||
$commit_message = & git log -1 --pretty=format:"%s"
|
||||
echo "::set-output name=commit_message::$commit_message"
|
||||
|
||||
- name: Build wails x go-stock
|
||||
uses: ArvinLovegood/wails-build-action@v3.4
|
||||
id: build
|
||||
with:
|
||||
build-name: ${{ matrix.build.name }}
|
||||
build-platform: ${{ matrix.build.platform }}
|
||||
package: true
|
||||
go-version: '1.23'
|
||||
build-tags: ${{ github.ref_name }}
|
||||
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
|
||||
node-version: '18.x'
|
||||
|
||||
8
.gitignore
vendored
@@ -106,6 +106,8 @@ dist
|
||||
.DS_Store
|
||||
|
||||
.idea/
|
||||
data/*.db
|
||||
build/*.exe
|
||||
/build/bin/go-stock-dev.exe
|
||||
/data/*.db
|
||||
/build/*.exe
|
||||
/build/bin/*
|
||||
frontend/package.json.md5
|
||||
/build/us.json
|
||||
|
||||
217
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,217 @@
|
||||
|
||||
# Contributor Covenant 行为准则
|
||||
|
||||
## 我们的承诺
|
||||
|
||||
我们作为项目的成员、贡献者和领导者,承诺为每一个人营造一个无骚扰的社区参与环境,无论年龄、体型、可见或不可见的残疾状况、种族、身体特征、性别认同与表达、经验水平、教育背景、社会经济地位、国籍、个人外貌、种族、宗教信仰或性取向如何。
|
||||
|
||||
我们承诺以有助于建立一个开放、友好、多元、包容和健康的社区的方式行事和互动。
|
||||
|
||||
## 我们的准则
|
||||
|
||||
有助于为我们的社区营造积极环境的行为示例包括:
|
||||
|
||||
- 对他人展现出同理心和善意
|
||||
- 尊重不同的意见、观点和经验
|
||||
- 给予并欣然接受建设性的反馈
|
||||
- 对自己的错误负责,向受影响的人道歉,并从经验中学习
|
||||
- 不仅关注个人利益,更着眼于整个社区的利益
|
||||
|
||||
不可接受的行为示例包括:
|
||||
|
||||
- 使用性暗示的语言或图像,以及任何形式的性关注或挑逗
|
||||
- 恶意挑衅、侮辱性或贬低性的评论,以及个人或政治攻击
|
||||
- 公开或私下的骚扰行为
|
||||
- 在未经明确许可的情况下公布他人的私人信息,如实际地址或电子邮件地址
|
||||
- 在专业环境中被合理认为不适当的其他行为
|
||||
|
||||
## 执行责任
|
||||
|
||||
社区领导者有责任阐明和执行我们可接受行为的标准,并将针对任何他们认为不适当、具有威胁性、冒犯性或有害的行为采取适当和公平的纠正措施。
|
||||
|
||||
社区领导者有权且有责任移除、编辑或拒绝不符合本行为准则的评论、提交的代码、代码修改、维基编辑、问题报告和其他贡献,并在适当时说明进行管理决策的原因。
|
||||
|
||||
## 适用范围
|
||||
|
||||
本行为准则适用于所有社区空间,并且当个人在公共场合正式代表社区时也同样适用。代表我们社区的示例包括使用官方电子邮件地址、通过官方社交媒体账户发布内容,或在线上或线下活动中担任指定代表。
|
||||
|
||||
## 执行
|
||||
|
||||
若发生滥用、骚扰或其他不可接受的行为,可向负责执行的社区领导者报告,邮箱地址为 [sparkmemory@163.com]。所有投诉都将得到及时、公正的审查和调查。
|
||||
|
||||
所有社区领导者都有义务尊重任何事件报告者的隐私和安全。
|
||||
|
||||
### 执行指南
|
||||
|
||||
社区领导者将遵循以下社区影响指南来确定对任何他们认为违反本行为准则的行为的后果:
|
||||
|
||||
#### 1. 纠正
|
||||
|
||||
**社区影响**:使用不适当的语言或其他被认为在社区中不专业或不受欢迎的行为。
|
||||
|
||||
**后果**:社区领导者发出私下的书面警告,阐明违规行为的性质,并解释为什么该行为不适当。可能会要求公开道歉。
|
||||
|
||||
#### 2. 警告
|
||||
|
||||
**社区影响**:通过单次事件或一系列行为构成的违规。
|
||||
|
||||
**后果**:发出警告并说明持续此类行为的后果。在指定的时间段内,禁止与相关人员进行互动,包括主动与执行本行为准则的人员进行互动。这包括避免在社区空间以及社交媒体等外部渠道进行互动。违反这些规定可能会导致临时或永久禁令。
|
||||
|
||||
#### 3. 临时禁令
|
||||
|
||||
**社区影响**:严重违反社区标准,包括持续的不当行为。
|
||||
|
||||
**后果**:在指定的时间段内,禁止与社区进行任何形式的互动或公开交流。在此期间,禁止与相关人员进行任何公开或私下的互动,包括主动与执行本行为准则的人员进行互动。违反这些规定可能会导致永久禁令。
|
||||
|
||||
#### 4. 永久禁令
|
||||
|
||||
**社区影响**:表现出违反社区标准的行为模式,包括持续的不当行为、骚扰个人,或对某类人群进行攻击或贬低。
|
||||
|
||||
**后果**:永久禁止在社区内进行任何形式的公开互动。
|
||||
|
||||
## 版权声明
|
||||
|
||||
本行为准则改编自 [Contributor Covenant][主页] 2.1 版本,可在 [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1] 查看。
|
||||
|
||||
社区影响指南的灵感来自 [Mozilla 的行为准则执行阶梯][Mozilla CoC]。
|
||||
|
||||
有关本行为准则常见问题的解答,请参阅常见问题解答页面 [https://www.contributor-covenant.org/faq][FAQ]。该准则有多种语言的翻译版本,可在 [https://www.contributor-covenant.org/translations][翻译] 查看。
|
||||
|
||||
[主页]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[翻译]: https://www.contributor-covenant.org/translations
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at [sparkmemory@163.com].
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
### Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
#### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
#### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
#### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
#### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
|
||||
at [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
79
CONTRIBUTING.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Contributing to [go-stock]
|
||||
|
||||
感谢你对 [go-stock] 项目的兴趣并愿意贡献代码!本指南将帮助你了解如何为这个项目做出贡献。
|
||||
|
||||
## 行为准则
|
||||
|
||||
在参与这个项目时,请遵守我们的 [行为准则](./CODE_OF_CONDUCT.md)。我们致力于为所有贡献者提供一个友好、包容和尊重的环境。
|
||||
|
||||
## 贡献类型
|
||||
|
||||
### 报告问题
|
||||
如果你发现了一个 bug、有功能请求或者对项目有任何建议,请在项目的 [GitHub Issues](https://github.com/ArvinLovegood/go-stock/issues) 中创建一个新的 issue。在创建 issue 时,请提供尽可能多的信息,包括:
|
||||
- **问题描述**:清晰地描述你遇到的问题或建议的功能。
|
||||
- **重现步骤**:如果是 bug,请提供重现该问题的具体步骤。
|
||||
- **环境信息**:例如操作系统、编程语言版本等。
|
||||
- **相关日志或错误信息**:如果有的话,请附上相关的日志或错误信息。
|
||||
|
||||
### 提交代码
|
||||
我们欢迎各种类型的代码贡献,包括修复 bug、添加新功能、改进文档等。请按照以下步骤提交你的代码:
|
||||
|
||||
#### 1. Fork 项目
|
||||
在 GitHub 上点击项目页面的 “Fork” 按钮,将项目复制到你自己的 GitHub 账户下。
|
||||
|
||||
#### 2. 克隆项目到本地
|
||||
使用以下命令将你 fork 的项目克隆到本地:
|
||||
```bash
|
||||
git clone https://github.com/ArvinLovegood/go-stock.git
|
||||
cd go-stock
|
||||
```
|
||||
|
||||
#### 3. 创建新分支
|
||||
在开始编写代码之前,创建一个新的分支来包含你的更改。建议使用一个描述性的分支名称,例如 `fix-bug-123` 或 `add-new-feature`。
|
||||
```bash
|
||||
git checkout -b 新分支名称
|
||||
```
|
||||
|
||||
#### 4. 编写代码
|
||||
在新分支上进行你的代码更改。请确保你的代码遵循项目的编码风格和规范。
|
||||
|
||||
#### 5. 测试代码
|
||||
在提交代码之前,请确保你的更改通过了项目的测试。如果项目没有测试,请考虑添加适当的测试。
|
||||
|
||||
#### 6. 提交更改
|
||||
将你的更改提交到本地仓库,并提供一个清晰、简洁的提交信息。
|
||||
```bash
|
||||
git add.
|
||||
git commit -m "描述你的更改,例如:修复了 #123 号 bug"
|
||||
```
|
||||
|
||||
#### 7. 同步上游仓库
|
||||
在推送代码之前,确保你的分支与上游仓库(原始项目)保持同步。
|
||||
```bash
|
||||
git remote add upstream https://github.com/ArvinLovegood/go-stock.git
|
||||
git fetch upstream
|
||||
git rebase upstream/main
|
||||
```
|
||||
|
||||
#### 8. 推送更改
|
||||
将你的更改推送到你 fork 的 GitHub 仓库。
|
||||
```bash
|
||||
git push origin 新分支名称
|
||||
```
|
||||
|
||||
#### 9. 创建 Pull Request
|
||||
在 GitHub 上,导航到你 fork 的项目页面,点击 “New pull request” 按钮。选择你刚刚推送的分支,并提供一个清晰的描述,说明你的更改内容和目的。然后提交 pull request。
|
||||
|
||||
### 改进文档
|
||||
良好的文档对于项目的成功至关重要。如果你发现文档中有错误、不清楚的地方或者有可以改进的地方,请提交一个 issue 或者直接修改文档并提交 pull request。
|
||||
|
||||
## 代码风格和规范
|
||||
请遵循项目的代码风格和规范。如果项目中没有明确的规范,请参考以下通用准则:
|
||||
- **代码格式**:使用一致的缩进、空格和换行符。
|
||||
- **注释**:添加适当的注释来解释代码的功能和逻辑。
|
||||
- **命名规范**:使用有意义的变量名、函数名和类名。
|
||||
|
||||
## 许可证
|
||||
通过贡献代码,你同意你的贡献将根据项目的 [许可证](./LICENSE) 进行分发。
|
||||
|
||||
再次感谢你对项目的贡献!如果你有任何问题或需要帮助,请随时在 issue 中提问。
|
||||
2
LICENSE
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright [2025] [sparkmemory@163.com]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
132
README.md
@@ -1,31 +1,127 @@
|
||||
# README
|
||||
# go-stock : 基于Wails和NaiveUI构建的AI赋能股票分析工具
|
||||
## 
|
||||

|
||||
[](https://github.com/ArvinLovegood/go-stock)
|
||||
[](https://gitee.com/arvinlovegood_admin/go-stock)
|
||||
[](https://gitcode.com/ArvinLovegood/go-stock)
|
||||
|
||||

|
||||
### 🌟公众号
|
||||

|
||||
|
||||
### 📈 交流群
|
||||
QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333)
|
||||
|
||||
|
||||
## About
|
||||
### ✨ 简介
|
||||
- 本项目基于Wails和NaiveUI开发,结合AI大模型构建的股票分析工具。
|
||||
- 目前已支持A股,港股,美股,未来计划加入基金,ETF等支持。
|
||||
- 支持市场整体/个股情绪分析,K线技术指标分析等功能。
|
||||
- 本项目仅供娱乐,不喜勿喷,AI分析股票结果仅供学习研究,投资有风险,请谨慎使用。
|
||||
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。
|
||||
|
||||
A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
|
||||
A股数据可视化工具,基于Wails和NaiveUI。
|
||||
### 📦 立即体验
|
||||
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
|
||||
## Prerequisites
|
||||
INSTALL [GO](https://golang.org) AND [Wails](https://wails.io/)
|
||||
|
||||
## Running the Application in Developer Mode
|
||||
The easiest way is to use the Wails CLI: `wails dev`
|
||||
### 💬 支持大模型/平台
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- |-----------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
||||
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner模型测试有问题,可通过本地模型或聚合模型平台使用 |
|
||||
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk),[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) |
|
||||
|
||||
This should hot refresh when making changes the Frontend and rebuild when making changes in the Go.
|
||||
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
|
||||
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定,注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- 火山方舟:每个模型注册即送50万tokens,[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
|
||||
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究,[注册链接](https://tushare.pro/register?reg=701944)
|
||||
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
|
||||
- 欢迎大家提出宝贵的建议,欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
|
||||
|
||||
|
||||
## 🧩 重大功能开发计划
|
||||
| 功能说明 | 状态 | 备注 |
|
||||
|-----------------|----|----------------------------------------------------------------------------------------------------------|
|
||||
| ETF支持 | 🚧 | ETF数据支持 (目前可以查看净值和估值) |
|
||||
| 美股支持 | ✅ | 美股数据支持 |
|
||||
| 港股支持 | ✅ | 港股数据支持 (目前有延迟) |
|
||||
| 多轮对话 | ✅ | AI分析后可继续对话提问 |
|
||||
| 自定义AI分析提问模板 | ✅ | 可配置的提问模板 [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha) |
|
||||
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
|
||||
|
||||
## 👀 更新日志
|
||||
### 2025.03.15 自定义爬虫使用的浏览器路径配置
|
||||
### 2025.03.14 优化编译构建,大幅减少编译后的程序文件大小
|
||||
### 2025.03.09 基金估值和净值监控查看
|
||||
### 2025.03.06 项目社区分享功能
|
||||
### 2025.02.28 美股数据支持
|
||||
### 2025.02.23 弹幕功能,盯盘不再孤单,无聊划个水!😎
|
||||
### 2025.02.22 港股数据支持(目前有延迟)
|
||||
|
||||
### 2025.02.16 AI分析后可继续对话提问
|
||||
- [v2025.2.16.1-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.16.1-alpha)
|
||||
|
||||
### 2025.02.12 可配置的提问模板
|
||||
- [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha)
|
||||
|
||||
|
||||
## 🦄 重大更新
|
||||
### BIG NEWS !!! 重大更新!!!
|
||||
- 2025.01.17 新增AI大模型分析股票功能
|
||||

|
||||
## 📸 功能截图
|
||||

|
||||
### 设置
|
||||

|
||||
### 成本设置
|
||||

|
||||
### 日K
|
||||

|
||||
### 分时
|
||||

|
||||
### 钉钉报警通知
|
||||

|
||||
### AI分析股票
|
||||

|
||||
### 版本信息提示
|
||||

|
||||
|
||||
## 💕 感谢以下项目
|
||||
- [NaiveUI](https://www.naiveui.com/)
|
||||
- [Wails](https://wails.io/)
|
||||
- [Vue](https://vuejs.org/)
|
||||
- [Vite](https://vitejs.dev/)
|
||||
- [Tushare](https://tushare.pro/register?reg=701944)
|
||||
|
||||
## 😘 赞助我
|
||||
### 都划到这了,如果我的项目对您有帮助,请赞助我吧!😊😊😊
|
||||
| 支付宝 | 微信 |
|
||||
|-----|-----|
|
||||
|  |  |
|
||||
|
||||
|
||||
## ⭐ Star History
|
||||
[](https://star-history.com/#ArvinLovegood/go-stock&Date)
|
||||
## 🤖 状态
|
||||

|
||||
|
||||
## 🐳 关于技术支持申明
|
||||
- 本软件基于开源技术构建,使用Wails、NaiveUI、Vue、AI大模型等开源项目。 技术上如有问题,可以先向对应的开源社区请求帮助。
|
||||
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系微信(备注 技术支持):ArvinLovegood
|
||||
|
||||
<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood">
|
||||
|
||||
|
||||
| 技术支持方式 | 赞助(元) |
|
||||
|:--------------------------------|:-----:|
|
||||
| 加 QQ:506808970,微信:ArvinLovegood | 100/次 |
|
||||
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |
|
||||
|
||||
## Building the Application for Production
|
||||
|
||||
You can build you Application with: `wails build`
|
||||
|
||||
|
||||
## License
|
||||
[Apache License 2.0](LICENSE)
|
||||
|
||||
## Credits
|
||||
[NaiveUI](https://www.naiveui.com/)
|
||||
[Wails](https://wails.io/)
|
||||
[Vue](https://vuejs.org/)
|
||||
[Vite](https://vitejs.dev/)
|
||||
56
SECURITY.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 安全策略
|
||||
|
||||
## 1. 受支持的版本
|
||||
|
||||
以下是 [go-stock] 项目当前接受安全更新支持的版本:
|
||||
|
||||
| 版本号 | 是否支持 |
|
||||
| ------- | ------------------ |
|
||||
| [v年.月.日.版本号] | :white_check_mark: |
|
||||
| [v较旧的年.月.日.版本号] | :x: |
|
||||
|
||||
请注意,通常只有最新的主要或次要版本会积极维护安全更新,较旧版本可能不会收到安全补丁。
|
||||
|
||||
## 2. 报告安全漏洞
|
||||
|
||||
### 2.1 报告方式
|
||||
我们非常重视安全漏洞问题。如果您在我们的项目中发现了安全漏洞,请通过以下方式向我们报告:
|
||||
|
||||
- **私下披露**:请发送电子邮件至 [sparkmemory@163.com]。在邮件中,请包含以下详细信息:
|
||||
- 对漏洞的详细描述,包括如何复现该漏洞。
|
||||
- 受影响的项目版本。
|
||||
- 该漏洞可能造成的任何影响或风险。
|
||||
- 如果可能,请提供建议的修复或缓解策略。
|
||||
|
||||
### 2.2 响应时间线
|
||||
- **首次确认**:我们将在收到报告后的 [7] 个工作日内确认收到您的报告。
|
||||
- **调查与进度更新**:我们将对报告的漏洞进行全面调查,并在 [7] 个工作日内向您提供调查进度更新。
|
||||
- **补丁发布**:一旦修复方案开发完成,我们将尽快发布补丁。发布补丁的时间可能会因漏洞的复杂程度而有所不同。
|
||||
|
||||
### 2.3 保密承诺
|
||||
我们深知安全漏洞报告保密的重要性。我们将对所有报告内容严格保密,未经您的许可,不会披露您的身份或漏洞的具体细节,除非法律有相关要求。
|
||||
|
||||
## 3. 安全更新与沟通
|
||||
|
||||
### 3.1 补丁发布
|
||||
当发现并修复安全漏洞后,我们会为受支持的项目版本发布补丁。补丁将在项目的官方 GitHub 仓库上提供。
|
||||
|
||||
### 3.2 安全公告
|
||||
我们会在项目的 GitHub 安全公告页面发布安全公告。这些公告将详细说明漏洞情况、受影响的版本以及缓解或修复问题的步骤。
|
||||
|
||||
### 3.3 沟通渠道
|
||||
- **GitHub**:所有关于安全更新和公告的官方通知将发布在项目的 GitHub 仓库上。
|
||||
- **电子邮件**:如果您订阅了项目的安全通知,您将收到有关重要安全更新的电子邮件通知。
|
||||
|
||||
## 4. 第三方依赖
|
||||
我们会定期审查和更新项目中使用的第三方依赖,以确保其安全性。然而,第三方组件的安全性也依赖于其各自的维护者。如果您发现与第三方依赖相关的安全问题,请同时向相应的维护者报告并告知我们。
|
||||
|
||||
## 5. 安全最佳实践
|
||||
我们鼓励项目的所有贡献者和用户遵循以下安全最佳实践:
|
||||
- 及时更新开发和生产环境,安装最新的安全补丁。
|
||||
- 使用强大的身份验证和授权机制。
|
||||
- 避免在代码中硬编码凭证信息。
|
||||
- 定期审查代码,排查潜在的安全漏洞。
|
||||
|
||||
## 6. 联系信息
|
||||
如果您对 [go-stock] 项目的安全策略有任何疑问或担忧,请通过 [sparkmemory@163.com] 联系我们。
|
||||
745
app.go
@@ -1,46 +1,497 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/getlantern/systray"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/go-toast/toast"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
cacheSize := 512 * 1024
|
||||
cache := freecache.NewCache(cacheSize)
|
||||
return &App{
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// startup is called at application startup
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData)
|
||||
})
|
||||
logger.SugaredLogger.Infof("Version:%s", Version)
|
||||
// Perform your setup here
|
||||
a.ctx = ctx
|
||||
|
||||
// 创建系统托盘
|
||||
systray.Run(func() {
|
||||
go onReady(a)
|
||||
}, func() {
|
||||
go onExit(a)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (a *App) CheckUpdate() {
|
||||
releaseVersion := &models.GitHubReleaseVersion{}
|
||||
_, err := resty.New().R().
|
||||
SetResult(releaseVersion).
|
||||
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
|
||||
if releaseVersion.TagName != Version {
|
||||
tag := &models.Tag{}
|
||||
_, err = resty.New().R().
|
||||
SetResult(tag).
|
||||
Get("https://api.github.com/repos/ArvinLovegood/go-stock/git/ref/tags/" + releaseVersion.TagName)
|
||||
if err == nil {
|
||||
releaseVersion.Tag = *tag
|
||||
}
|
||||
commit := &models.Commit{}
|
||||
_, err = resty.New().R().
|
||||
SetResult(commit).
|
||||
Get(tag.Object.Url)
|
||||
if err == nil {
|
||||
releaseVersion.Commit = *commit
|
||||
}
|
||||
|
||||
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// domReady is called after front-end resources have been loaded
|
||||
func (a App) domReady(ctx context.Context) {
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
|
||||
// Add your action here
|
||||
//定时更新数据
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
go func() {
|
||||
interval := config.RefreshInterval
|
||||
if interval <= 0 {
|
||||
interval = 1
|
||||
}
|
||||
ticker := time.NewTicker(time.Second * time.Duration(interval))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
MonitorStockPrices(a)
|
||||
}
|
||||
}()
|
||||
|
||||
//刷新基金净值信息
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
MonitorFundPrices(a)
|
||||
}
|
||||
}()
|
||||
|
||||
if config.EnableNews {
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
telegraph := refreshTelegraphList()
|
||||
if telegraph != nil {
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
|
||||
}
|
||||
go MonitorStockPrices(a)
|
||||
go MonitorFundPrices(a)
|
||||
go data.NewFundApi().AllFund()
|
||||
//检查新版本
|
||||
go func() {
|
||||
a.CheckUpdate()
|
||||
}()
|
||||
|
||||
//检查谷歌浏览器
|
||||
//go func() {
|
||||
// f := checkChromeOnWindows()
|
||||
// if !f {
|
||||
// go runtime.EventsEmit(a.ctx, "warnMsg", "谷歌浏览器未安装,ai分析功能可能无法使用")
|
||||
// }
|
||||
//}()
|
||||
|
||||
//检查Edge浏览器
|
||||
//go func() {
|
||||
// path, e := checkEdgeOnWindows()
|
||||
// if !e {
|
||||
// go runtime.EventsEmit(a.ctx, "warnMsg", "Edge浏览器未安装,ai分析功能可能无法使用")
|
||||
// } else {
|
||||
// logger.SugaredLogger.Infof("Edge浏览器已安装,路径为: %s", path)
|
||||
// }
|
||||
//}()
|
||||
}
|
||||
|
||||
func refreshTelegraphList() *[]string {
|
||||
url := "https://www.cls.cn/telegraph"
|
||||
response, err := resty.New().R().
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(fmt.Sprintf(url))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
//logger.SugaredLogger.Info(string(response.Body()))
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
var telegraph []string
|
||||
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
|
||||
//logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph = append(telegraph, selection.Text())
|
||||
})
|
||||
return &telegraph
|
||||
}
|
||||
|
||||
// isTradingDay 判断是否是交易日
|
||||
func isTradingDay(date time.Time) bool {
|
||||
weekday := date.Weekday()
|
||||
// 判断是否是周末
|
||||
if weekday == time.Saturday || weekday == time.Sunday {
|
||||
return false
|
||||
}
|
||||
// 这里可以添加具体的节假日判断逻辑
|
||||
// 例如:判断是否是春节、国庆节等
|
||||
return true
|
||||
}
|
||||
|
||||
// isTradingTime 判断是否是交易时间
|
||||
func isTradingTime(date time.Time) bool {
|
||||
if !isTradingDay(date) {
|
||||
return false
|
||||
}
|
||||
|
||||
hour, minute, _ := date.Clock()
|
||||
|
||||
// 判断是否在9:15到11:30之间
|
||||
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 判断是否在13:00到15:00之间
|
||||
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsHKTradingTime 判断当前时间是否在港股交易时间内
|
||||
func IsHKTradingTime(date time.Time) bool {
|
||||
hour, minute, _ := date.Clock()
|
||||
|
||||
// 开市前竞价时段:09:00 - 09:30
|
||||
if (hour == 9 && minute >= 0) || (hour == 9 && minute <= 30) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 上午持续交易时段:09:30 - 12:00
|
||||
if (hour == 9 && minute > 30) || (hour >= 10 && hour < 12) || (hour == 12 && minute == 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 下午持续交易时段:13:00 - 16:00
|
||||
if (hour == 13 && minute >= 0) || (hour >= 14 && hour < 16) || (hour == 16 && minute == 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 收市竞价交易时段:16:00 - 16:10
|
||||
if (hour == 16 && minute >= 0) || (hour == 16 && minute <= 10) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsUSTradingTime 判断当前时间是否在美股交易时间内
|
||||
func IsUSTradingTime(date time.Time) bool {
|
||||
// 获取美国东部时区
|
||||
est, err := time.LoadLocation("America/New_York")
|
||||
var estTime time.Time
|
||||
if err != nil {
|
||||
estTime = date.Add(time.Hour * -12)
|
||||
} else {
|
||||
// 将当前时间转换为美国东部时间
|
||||
estTime = date.In(est)
|
||||
}
|
||||
|
||||
// 判断是否是周末
|
||||
weekday := estTime.Weekday()
|
||||
if weekday == time.Saturday || weekday == time.Sunday {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取小时和分钟
|
||||
hour, minute, _ := estTime.Clock()
|
||||
|
||||
// 判断是否在4:00 AM到9:30 AM之间(盘前)
|
||||
if (hour == 4) || (hour == 5) || (hour == 6) || (hour == 7) || (hour == 8) || (hour == 9 && minute < 30) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 判断是否在9:30 AM到4:00 PM之间(盘中)
|
||||
if (hour == 9 && minute >= 30) || (hour >= 10 && hour < 16) || (hour == 16 && minute == 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 判断是否在4:00 PM到8:00 PM之间(盘后)
|
||||
if (hour == 16 && minute > 0) || (hour >= 17 && hour < 20) || (hour == 20 && minute == 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
func MonitorFundPrices(a *App) {
|
||||
dest := &[]data.FollowedFund{}
|
||||
db.Dao.Model(&data.FollowedFund{}).Find(dest)
|
||||
for _, follow := range *dest {
|
||||
_, err := data.NewFundApi().CrawlFundBasic(follow.Code)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("获取基金基本信息失败,基金代码:%s,错误信息:%s", follow.Code, err.Error())
|
||||
continue
|
||||
}
|
||||
data.NewFundApi().CrawlFundNetEstimatedUnit(follow.Code)
|
||||
data.NewFundApi().CrawlFundNetUnitValue(follow.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func MonitorStockPrices(a *App) {
|
||||
dest := &[]data.FollowedStock{}
|
||||
db.Dao.Model(&data.FollowedStock{}).Find(dest)
|
||||
total := float64(0)
|
||||
//for _, follow := range *dest {
|
||||
// stockData := getStockInfo(follow)
|
||||
// total += stockData.ProfitAmountToday
|
||||
// price, _ := convertor.ToFloat(stockData.Price)
|
||||
// if stockData.PrePrice != price {
|
||||
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
|
||||
// }
|
||||
//}
|
||||
|
||||
stockInfos := GetStockInfos(*dest...)
|
||||
for _, stockInfo := range *stockInfos {
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
|
||||
total += stockInfo.ProfitAmountToday
|
||||
price, _ := convertor.ToFloat(stockInfo.Price)
|
||||
if stockInfo.PrePrice != price {
|
||||
logger.SugaredLogger.Infof("-----------------------股票代码: %s, 股票名称: %s, 股票价格: %s,盘前盘后:%s", stockInfo.Code, stockInfo.Name, stockInfo.Price, stockInfo.BA)
|
||||
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
|
||||
}
|
||||
}
|
||||
if total != 0 {
|
||||
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
|
||||
systray.SetTooltip(title)
|
||||
}
|
||||
|
||||
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
|
||||
//runtime.WindowSetTitle(a.ctx, title)
|
||||
|
||||
}
|
||||
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
|
||||
stockInfos := make([]data.StockInfo, 0)
|
||||
stockCodes := make([]string, 0)
|
||||
for _, follow := range follows {
|
||||
if strutil.HasPrefixAny(follow.StockCode, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(follow.StockCode, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(follow.StockCode, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
stockCodes = append(stockCodes, follow.StockCode)
|
||||
}
|
||||
stockData, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
|
||||
for _, info := range *stockData {
|
||||
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
|
||||
if strutil.HasPrefixAny(follow.StockCode, []string{"US", "us"}) {
|
||||
return strings.ToLower(strings.Replace(follow.StockCode, "us", "gb_", 1)) == info.Code
|
||||
}
|
||||
|
||||
return follow.StockCode == info.Code
|
||||
})
|
||||
if ok {
|
||||
addStockFollowData(v, &info)
|
||||
stockInfos = append(stockInfos, info)
|
||||
}
|
||||
}
|
||||
return &stockInfos
|
||||
}
|
||||
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
|
||||
stockCode := follow.StockCode
|
||||
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
if err != nil || len(*stockDatas) == 0 {
|
||||
return &data.StockInfo{}
|
||||
}
|
||||
stockData := (*stockDatas)[0]
|
||||
addStockFollowData(follow, &stockData)
|
||||
return &stockData
|
||||
}
|
||||
|
||||
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
|
||||
stockData.PrePrice = follow.Price //上次当前价格
|
||||
stockData.Sort = follow.Sort
|
||||
stockData.CostPrice = follow.CostPrice //成本价
|
||||
stockData.CostVolume = follow.Volume //成本量
|
||||
stockData.AlarmChangePercent = follow.AlarmChangePercent
|
||||
stockData.AlarmPrice = follow.AlarmPrice
|
||||
|
||||
//当前价格
|
||||
price, _ := convertor.ToFloat(stockData.Price)
|
||||
//当前价格为0 时 使用卖一价格作为当前价格
|
||||
if price == 0 {
|
||||
price, _ = convertor.ToFloat(stockData.A1P)
|
||||
}
|
||||
//当前价格依然为0 时 使用买一报价作为当前价格
|
||||
if price == 0 {
|
||||
price, _ = convertor.ToFloat(stockData.B1P)
|
||||
}
|
||||
|
||||
//昨日收盘价
|
||||
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
|
||||
|
||||
//当前价格依然为0 时 使用昨日收盘价为当前价格
|
||||
if price == 0 {
|
||||
price = preClosePrice
|
||||
}
|
||||
|
||||
//今日最高价
|
||||
highPrice, _ := convertor.ToFloat(stockData.High)
|
||||
if highPrice == 0 {
|
||||
highPrice, _ = convertor.ToFloat(stockData.Open)
|
||||
}
|
||||
|
||||
//今日最低价
|
||||
lowPrice, _ := convertor.ToFloat(stockData.Low)
|
||||
if lowPrice == 0 {
|
||||
lowPrice, _ = convertor.ToFloat(stockData.Open)
|
||||
}
|
||||
//开盘价
|
||||
//openPrice, _ := convertor.ToFloat(stockData.Open)
|
||||
|
||||
if price > 0 && preClosePrice > 0 {
|
||||
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
|
||||
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if highPrice > 0 && preClosePrice > 0 {
|
||||
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if lowPrice > 0 && preClosePrice > 0 {
|
||||
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if follow.CostPrice > 0 && follow.Volume > 0 {
|
||||
if price > 0 {
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
|
||||
} else {
|
||||
//未开盘时当前价格为昨日收盘价
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
|
||||
if follow.Price != price && price > 0 {
|
||||
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
|
||||
"price": price,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// beforeClose is called when the application is about to quit,
|
||||
// either by clicking the window close button or calling runtime.Quit.
|
||||
// Returning true will cause the application to continue, false will continue shutdown as normal.
|
||||
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
defer PanicHandler()
|
||||
|
||||
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.QuestionDialog,
|
||||
Title: "go-stock",
|
||||
Message: "确定关闭吗?",
|
||||
Buttons: []string{"确定"},
|
||||
Icon: icon,
|
||||
CancelButton: "取消",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
|
||||
return false
|
||||
}
|
||||
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
||||
if dialog == "No" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shutdown is called at application termination
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
// Perform your teardown here
|
||||
systray.Quit()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(name string) *data.StockInfo {
|
||||
stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(name)
|
||||
func (a *App) Greet(stockCode string) *data.StockInfo {
|
||||
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
|
||||
follow := &data.FollowedStock{
|
||||
StockCode: stockCode,
|
||||
}
|
||||
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
|
||||
stockInfo := getStockInfo(*follow)
|
||||
return stockInfo
|
||||
}
|
||||
|
||||
@@ -52,7 +503,7 @@ func (a *App) UnFollow(stockCode string) string {
|
||||
return data.NewStockDataApi().UnFollow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) GetFollowList() []data.FollowedStock {
|
||||
func (a *App) GetFollowList() *[]data.FollowedStock {
|
||||
return data.NewStockDataApi().GetFollowList()
|
||||
}
|
||||
|
||||
@@ -63,3 +514,285 @@ func (a *App) GetStockList(key string) []data.StockBasic {
|
||||
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
|
||||
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
|
||||
}
|
||||
|
||||
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
|
||||
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
|
||||
}
|
||||
func (a *App) SetStockSort(sort int64, stockCode string) {
|
||||
data.NewStockDataApi().SetStockSort(sort, stockCode)
|
||||
}
|
||||
func (a *App) SendDingDingMessage(message string, stockCode string) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
|
||||
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
|
||||
|
||||
if strutil.HasPrefixAny(stockCode, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
|
||||
return "非A股交易时间"
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
|
||||
return "非港股交易时间"
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
|
||||
return "非美股交易时间"
|
||||
}
|
||||
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
//logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
stockInfo := &data.StockInfo{}
|
||||
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
|
||||
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func (a *App) NewChatStream(stock, stockCode, question string) {
|
||||
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question)
|
||||
for msg := range msgs {
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", msg)
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
|
||||
}
|
||||
|
||||
func (a *App) SaveAIResponseResult(stockCode, stockName, result, chatId, question string) {
|
||||
data.NewDeepSeekOpenAi(a.ctx).SaveAIResponseResult(stockCode, stockName, result, chatId, question)
|
||||
}
|
||||
func (a *App) GetAIResponseResult(stock string) *models.AIResponseResult {
|
||||
return data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stock)
|
||||
}
|
||||
|
||||
func (a *App) GetVersionInfo() *models.VersionInfo {
|
||||
return &models.VersionInfo{
|
||||
Version: Version,
|
||||
Icon: GetImageBase(icon),
|
||||
Alipay: GetImageBase(alipay),
|
||||
Wxpay: GetImageBase(wxpay),
|
||||
Content: VersionCommit,
|
||||
}
|
||||
}
|
||||
|
||||
// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装
|
||||
func checkChromeOnWindows() bool {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer key.Close()
|
||||
}
|
||||
defer key.Close()
|
||||
_, _, err = key.GetValue("Path", nil)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// checkEdgeOnWindows 在 Windows 系统上检查Edge浏览器是否安装,并返回安装路径
|
||||
func checkEdgeOnWindows() (string, bool) {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
defer key.Close()
|
||||
}
|
||||
defer key.Close()
|
||||
path, _, err := key.GetStringValue("Path")
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
|
||||
func GetImageBase(bytes []byte) string {
|
||||
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func GenNotificationMsg(stockInfo *data.StockInfo) string {
|
||||
Price, err := convertor.ToFloat(stockInfo.Price)
|
||||
if err != nil {
|
||||
Price = 0
|
||||
}
|
||||
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
|
||||
if err != nil {
|
||||
PreClose = 0
|
||||
}
|
||||
var RF float64
|
||||
if PreClose > 0 {
|
||||
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
|
||||
}
|
||||
|
||||
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
|
||||
}
|
||||
|
||||
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
|
||||
func getMsgTypeTTL(msgType int) int {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return 60 * 5
|
||||
case 2:
|
||||
return 60 * 30
|
||||
case 3:
|
||||
return 60 * 30
|
||||
default:
|
||||
return 60 * 5
|
||||
}
|
||||
}
|
||||
|
||||
func getMsgTypeName(msgType int) string {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return "涨跌报警"
|
||||
case 2:
|
||||
return "股价报警"
|
||||
case 3:
|
||||
return "成本价报警"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
|
||||
func onExit(a *App) {
|
||||
// 清理操作
|
||||
logger.SugaredLogger.Infof("onExit")
|
||||
systray.Quit()
|
||||
runtime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
func onReady(a *App) {
|
||||
|
||||
// 初始化操作
|
||||
logger.SugaredLogger.Infof("onReady")
|
||||
systray.SetIcon(icon2)
|
||||
systray.SetTitle("go-stock")
|
||||
systray.SetTooltip("go-stock 股票行情实时获取")
|
||||
// 创建菜单项
|
||||
show := systray.AddMenuItem("显示", "显示应用程序")
|
||||
hide := systray.AddMenuItem("隐藏", "隐藏应用程序")
|
||||
systray.AddSeparator()
|
||||
mQuitOrig := systray.AddMenuItem("退出", "退出应用程序")
|
||||
|
||||
// 监听菜单项点击事件
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-mQuitOrig.ClickedCh:
|
||||
logger.SugaredLogger.Infof("退出应用程序")
|
||||
runtime.Quit(a.ctx)
|
||||
case <-show.ClickedCh:
|
||||
logger.SugaredLogger.Infof("显示应用程序")
|
||||
runtime.WindowShow(a.ctx)
|
||||
case <-hide.ClickedCh:
|
||||
logger.SugaredLogger.Infof("隐藏应用程序")
|
||||
runtime.WindowHide(a.ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() *data.Settings {
|
||||
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
}
|
||||
|
||||
func (a *App) ExportConfig() string {
|
||||
config := data.NewSettingsApi(&data.Settings{}).Export()
|
||||
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
Title: "导出配置文件",
|
||||
CanCreateDirectories: true,
|
||||
DefaultFilename: "config.json",
|
||||
})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("导出配置文件失败:%s", err.Error())
|
||||
return err.Error()
|
||||
}
|
||||
err = os.WriteFile(file, []byte(config), 0644)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("导出配置文件失败:%s", err.Error())
|
||||
return err.Error()
|
||||
}
|
||||
return "导出成功:" + file
|
||||
}
|
||||
func getScreenResolution() (int, int, error) {
|
||||
//user32 := syscall.NewLazyDLL("user32.dll")
|
||||
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
|
||||
//
|
||||
//width, _, _ := getSystemMetrics.Call(0)
|
||||
//height, _, _ := getSystemMetrics.Call(1)
|
||||
|
||||
return int(1366), int(768), nil
|
||||
}
|
||||
|
||||
func (a *App) ShareAnalysis(stockCode, stockName string) string {
|
||||
//http://go-stock.sparkmemory.top:16688/upload
|
||||
res := data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stockCode)
|
||||
if res != nil && len(res.Content) > 100 {
|
||||
analysisTime := res.CreatedAt.Format("2006/01/02")
|
||||
logger.SugaredLogger.Infof("%s analysisTime:%s", res.CreatedAt, analysisTime)
|
||||
response, err := resty.New().SetHeader("ua-x", "go-stock").R().SetFormData(map[string]string{
|
||||
"text": res.Content,
|
||||
"stockCode": stockCode,
|
||||
"stockName": stockName,
|
||||
"analysisTime": analysisTime,
|
||||
}).Post("http://go-stock.sparkmemory.top:16688/upload")
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return response.String()
|
||||
} else {
|
||||
return "分析结果异常"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) GetfundList(key string) []data.FundBasic {
|
||||
return data.NewFundApi().GetFundList(key)
|
||||
}
|
||||
func (a *App) GetFollowedFund() []data.FollowedFund {
|
||||
return data.NewFundApi().GetFollowedFund()
|
||||
}
|
||||
func (a *App) FollowFund(fundCode string) string {
|
||||
return data.NewFundApi().FollowFund(fundCode)
|
||||
}
|
||||
func (a *App) UnFollowFund(fundCode string) string {
|
||||
return data.NewFundApi().UnFollowFund(fundCode)
|
||||
}
|
||||
|
||||
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
notification := toast.Notification{
|
||||
AppID: "go-stock",
|
||||
Title: "go-stock",
|
||||
Message: "程序已经在运行了",
|
||||
Icon: "",
|
||||
Duration: "short",
|
||||
Audio: toast.Default,
|
||||
}
|
||||
err := notification.Push()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
}
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
|
||||
459
app_darwin.go
Normal file
@@ -0,0 +1,459 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
cacheSize := 512 * 1024
|
||||
cache := freecache.NewCache(cacheSize)
|
||||
return &App{
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// startup is called at application startup
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
logger.SugaredLogger.Infof("Version:%s", Version)
|
||||
// Perform your setup here
|
||||
a.ctx = ctx
|
||||
|
||||
// TODO 创建系统托盘
|
||||
|
||||
}
|
||||
|
||||
func checkUpdate(a *App) {
|
||||
releaseVersion := &models.GitHubReleaseVersion{}
|
||||
_, err := resty.New().R().
|
||||
SetResult(releaseVersion).
|
||||
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
|
||||
if releaseVersion.TagName != Version {
|
||||
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// domReady is called after front-end resources have been loaded
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
// Add your action here
|
||||
//定时更新数据
|
||||
go func() {
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
interval := config.RefreshInterval
|
||||
if interval <= 0 {
|
||||
interval = 1
|
||||
}
|
||||
ticker := time.NewTicker(time.Second * time.Duration(interval))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if isTradingTime(time.Now()) {
|
||||
MonitorStockPrices(a)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
telegraph := refreshTelegraphList()
|
||||
if telegraph != nil {
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
|
||||
go MonitorStockPrices(a)
|
||||
|
||||
//检查新版本
|
||||
go func() {
|
||||
checkUpdate(a)
|
||||
}()
|
||||
}
|
||||
|
||||
func refreshTelegraphList() *[]string {
|
||||
url := "https://www.cls.cn/telegraph"
|
||||
response, err := resty.New().R().
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(fmt.Sprintf(url))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
//logger.SugaredLogger.Info(string(response.Body()))
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
var telegraph []string
|
||||
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
|
||||
//logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph = append(telegraph, selection.Text())
|
||||
})
|
||||
return &telegraph
|
||||
}
|
||||
|
||||
// isTradingDay 判断是否是交易日
|
||||
func isTradingDay(date time.Time) bool {
|
||||
weekday := date.Weekday()
|
||||
// 判断是否是周末
|
||||
if weekday == time.Saturday || weekday == time.Sunday {
|
||||
return false
|
||||
}
|
||||
// 这里可以添加具体的节假日判断逻辑
|
||||
// 例如:判断是否是春节、国庆节等
|
||||
return true
|
||||
}
|
||||
|
||||
// isTradingTime 判断是否是交易时间
|
||||
func isTradingTime(date time.Time) bool {
|
||||
if !isTradingDay(date) {
|
||||
return false
|
||||
}
|
||||
|
||||
hour, minute, _ := date.Clock()
|
||||
|
||||
// 判断是否在9:15到11:30之间
|
||||
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 判断是否在13:00到15:00之间
|
||||
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func MonitorStockPrices(a *App) {
|
||||
dest := &[]data.FollowedStock{}
|
||||
db.Dao.Model(&data.FollowedStock{}).Find(dest)
|
||||
total := float64(0)
|
||||
//for _, follow := range *dest {
|
||||
// stockData := getStockInfo(follow)
|
||||
// total += stockData.ProfitAmountToday
|
||||
// price, _ := convertor.ToFloat(stockData.Price)
|
||||
// if stockData.PrePrice != price {
|
||||
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
|
||||
// }
|
||||
//}
|
||||
|
||||
stockInfos := GetStockInfos(*dest...)
|
||||
for _, stockInfo := range *stockInfos {
|
||||
total += stockInfo.ProfitAmountToday
|
||||
price, _ := convertor.ToFloat(stockInfo.Price)
|
||||
if stockInfo.PrePrice != price {
|
||||
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
|
||||
}
|
||||
}
|
||||
if total != 0 {
|
||||
// title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
|
||||
// systray.SetTooltip(title)
|
||||
}
|
||||
|
||||
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
|
||||
//runtime.WindowSetTitle(a.ctx, title)
|
||||
|
||||
}
|
||||
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
|
||||
stockCodes := make([]string, 0)
|
||||
for _, follow := range follows {
|
||||
stockCodes = append(stockCodes, follow.StockCode)
|
||||
}
|
||||
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
|
||||
return nil
|
||||
}
|
||||
stockInfos := make([]data.StockInfo, 0)
|
||||
for _, info := range *stockData {
|
||||
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
|
||||
return follow.StockCode == info.Code
|
||||
})
|
||||
if ok {
|
||||
addStockFollowData(v, &info)
|
||||
stockInfos = append(stockInfos, info)
|
||||
}
|
||||
}
|
||||
return &stockInfos
|
||||
}
|
||||
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
|
||||
stockCode := follow.StockCode
|
||||
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
if err != nil || len(*stockDatas) == 0 {
|
||||
return &data.StockInfo{}
|
||||
}
|
||||
stockData := (*stockDatas)[0]
|
||||
addStockFollowData(follow, &stockData)
|
||||
return &stockData
|
||||
}
|
||||
|
||||
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
|
||||
stockData.PrePrice = follow.Price //上次当前价格
|
||||
stockData.Sort = follow.Sort
|
||||
stockData.CostPrice = follow.CostPrice //成本价
|
||||
stockData.CostVolume = follow.Volume //成本量
|
||||
stockData.AlarmChangePercent = follow.AlarmChangePercent
|
||||
stockData.AlarmPrice = follow.AlarmPrice
|
||||
|
||||
//当前价格
|
||||
price, _ := convertor.ToFloat(stockData.Price)
|
||||
//当前价格为0 时 使用卖一价格作为当前价格
|
||||
if price == 0 {
|
||||
price, _ = convertor.ToFloat(stockData.A1P)
|
||||
}
|
||||
//当前价格依然为0 时 使用买一报价作为当前价格
|
||||
if price == 0 {
|
||||
price, _ = convertor.ToFloat(stockData.B1P)
|
||||
}
|
||||
|
||||
//昨日收盘价
|
||||
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
|
||||
|
||||
//当前价格依然为0 时 使用昨日收盘价为当前价格
|
||||
if price == 0 {
|
||||
price = preClosePrice
|
||||
}
|
||||
|
||||
//今日最高价
|
||||
highPrice, _ := convertor.ToFloat(stockData.High)
|
||||
if highPrice == 0 {
|
||||
highPrice, _ = convertor.ToFloat(stockData.Open)
|
||||
}
|
||||
|
||||
//今日最低价
|
||||
lowPrice, _ := convertor.ToFloat(stockData.Low)
|
||||
if lowPrice == 0 {
|
||||
lowPrice, _ = convertor.ToFloat(stockData.Open)
|
||||
}
|
||||
//开盘价
|
||||
//openPrice, _ := convertor.ToFloat(stockData.Open)
|
||||
|
||||
if price > 0 {
|
||||
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
|
||||
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if highPrice > 0 {
|
||||
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if lowPrice > 0 {
|
||||
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if follow.CostPrice > 0 && follow.Volume > 0 {
|
||||
if price > 0 {
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
|
||||
} else {
|
||||
//未开盘时当前价格为昨日收盘价
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
|
||||
if follow.Price != price && price > 0 {
|
||||
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
|
||||
"price": price,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// beforeClose is called when the application is about to quit,
|
||||
// either by clicking the window close button or calling runtime.Quit.
|
||||
// Returning true will cause the application to continue, false will continue shutdown as normal.
|
||||
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
|
||||
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.QuestionDialog,
|
||||
Title: "go-stock",
|
||||
Message: "确定关闭吗?",
|
||||
Buttons: []string{"确定"},
|
||||
Icon: icon,
|
||||
CancelButton: "取消",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
|
||||
return false
|
||||
}
|
||||
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
||||
if dialog == "No" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shutdown is called at application termination
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
// Perform your teardown here
|
||||
// systray.Quit()
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(stockCode string) *data.StockInfo {
|
||||
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
|
||||
follow := &data.FollowedStock{
|
||||
StockCode: stockCode,
|
||||
}
|
||||
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
|
||||
stockInfo := getStockInfo(*follow)
|
||||
return stockInfo
|
||||
}
|
||||
|
||||
func (a *App) Follow(stockCode string) string {
|
||||
return data.NewStockDataApi().Follow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) UnFollow(stockCode string) string {
|
||||
return data.NewStockDataApi().UnFollow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) GetFollowList() []data.FollowedStock {
|
||||
return data.NewStockDataApi().GetFollowList()
|
||||
}
|
||||
|
||||
func (a *App) GetStockList(key string) []data.StockBasic {
|
||||
return data.NewStockDataApi().GetStockList(key)
|
||||
}
|
||||
|
||||
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
|
||||
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
|
||||
}
|
||||
|
||||
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
|
||||
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
|
||||
}
|
||||
func (a *App) SetStockSort(sort int64, stockCode string) {
|
||||
data.NewStockDataApi().SetStockSort(sort, stockCode)
|
||||
}
|
||||
func (a *App) SendDingDingMessage(message string, stockCode string) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
|
||||
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
stockInfo := &data.StockInfo{}
|
||||
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
|
||||
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func (a *App) NewChat(stock string) string {
|
||||
return data.NewDeepSeekOpenAi().NewChat(stock)
|
||||
}
|
||||
|
||||
func (a *App) NewChatStream(stock, stockCode string) {
|
||||
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
|
||||
for msg := range msgs {
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", msg)
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
|
||||
}
|
||||
|
||||
func GenNotificationMsg(stockInfo *data.StockInfo) string {
|
||||
Price, err := convertor.ToFloat(stockInfo.Price)
|
||||
if err != nil {
|
||||
Price = 0
|
||||
}
|
||||
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
|
||||
if err != nil {
|
||||
PreClose = 0
|
||||
}
|
||||
var RF float64
|
||||
if PreClose > 0 {
|
||||
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
|
||||
}
|
||||
|
||||
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
|
||||
}
|
||||
|
||||
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
|
||||
func getMsgTypeTTL(msgType int) int {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return 60 * 5
|
||||
case 2:
|
||||
return 60 * 30
|
||||
case 3:
|
||||
return 60 * 30
|
||||
default:
|
||||
return 60 * 5
|
||||
}
|
||||
}
|
||||
|
||||
func getMsgTypeName(msgType int) string {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return "涨跌报警"
|
||||
case 2:
|
||||
return "股价报警"
|
||||
case 3:
|
||||
return "成本价报警"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() *data.Settings {
|
||||
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
}
|
||||
193
app_linux.go
Normal file
@@ -0,0 +1,193 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/logger"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
cacheSize := 512 * 1024
|
||||
cache := freecache.NewCache(cacheSize)
|
||||
return &App{
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// startup is called at application startup
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
// Perform your setup here
|
||||
a.ctx = ctx
|
||||
|
||||
}
|
||||
|
||||
// domReady is called after front-end resources have been loaded
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
// Add your action here
|
||||
//ticker := time.NewTicker(time.Second)
|
||||
//defer ticker.Stop()
|
||||
////定时更新数据
|
||||
//go func() {
|
||||
// for range ticker.C {
|
||||
// runtime.WindowSetTitle(ctx, "go-stock "+time.Now().Format("2006-01-02 15:04:05"))
|
||||
// }
|
||||
//}()
|
||||
}
|
||||
|
||||
// beforeClose is called when the application is about to quit,
|
||||
// either by clicking the window close button or calling runtime.Quit.
|
||||
// Returning true will cause the application to continue, false will continue shutdown as normal.
|
||||
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
|
||||
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.QuestionDialog,
|
||||
Title: "go-stock",
|
||||
Message: "确定关闭吗?",
|
||||
Buttons: []string{"确定"},
|
||||
Icon: icon,
|
||||
CancelButton: "取消",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
||||
if dialog == "No" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shutdown is called at application termination
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
// Perform your teardown here
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(name string) *data.StockInfo {
|
||||
stockDatas, _ := data.NewStockDataApi().GetStockCodeRealTimeData(name)
|
||||
stockData := (*stockDatas)[0]
|
||||
return &stockData
|
||||
}
|
||||
|
||||
func (a *App) Follow(stockCode string) string {
|
||||
return data.NewStockDataApi().Follow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) UnFollow(stockCode string) string {
|
||||
return data.NewStockDataApi().UnFollow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) GetFollowList() []data.FollowedStock {
|
||||
return data.NewStockDataApi().GetFollowList()
|
||||
}
|
||||
|
||||
func (a *App) GetStockList(key string) []data.StockBasic {
|
||||
return data.NewStockDataApi().GetStockList(key)
|
||||
}
|
||||
|
||||
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
|
||||
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
|
||||
}
|
||||
|
||||
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
|
||||
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
|
||||
}
|
||||
|
||||
func (a *App) SendDingDingMessage(message string, stockCode string) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func (a *App) SetStockSort(sort int64, stockCode string) {
|
||||
data.NewStockDataApi().SetStockSort(sort, stockCode)
|
||||
}
|
||||
|
||||
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
|
||||
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func GenNotificationMsg(stockInfo *data.StockInfo) string {
|
||||
Price, err := convertor.ToFloat(stockInfo.Price)
|
||||
if err != nil {
|
||||
Price = 0
|
||||
}
|
||||
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
|
||||
if err != nil {
|
||||
PreClose = 0
|
||||
}
|
||||
var RF float64
|
||||
if PreClose > 0 {
|
||||
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
|
||||
}
|
||||
|
||||
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
|
||||
}
|
||||
|
||||
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
|
||||
func getMsgTypeTTL(msgType int) int {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return 60 * 5
|
||||
case 2:
|
||||
return 60 * 30
|
||||
case 3:
|
||||
return 60 * 30
|
||||
default:
|
||||
return 60 * 5
|
||||
}
|
||||
}
|
||||
|
||||
func getMsgTypeName(msgType int) string {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return "涨跌报警"
|
||||
case 2:
|
||||
return "股价报警"
|
||||
case 3:
|
||||
return "成本价报警"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() *data.Settings {
|
||||
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
}
|
||||
25
app_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/24 9:35
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
func TestIsHKTradingTime(t *testing.T) {
|
||||
f := IsHKTradingTime(time.Now())
|
||||
t.Log(f)
|
||||
}
|
||||
|
||||
func TestIsUSTradingTime(t *testing.T) {
|
||||
|
||||
date := time.Now()
|
||||
hour, minute, _ := date.Clock()
|
||||
logger.SugaredLogger.Infof("当前时间: %d:%d", hour, minute)
|
||||
|
||||
t.Log(IsUSTradingTime(time.Now()))
|
||||
}
|
||||
52
backend/data/alert_darwin_api.go
Normal file
@@ -0,0 +1,52 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-stock/backend/logger"
|
||||
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// AlertWindowsApi @Author 2lovecode
|
||||
// @Date 2025/02/06 17:50
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type AlertWindowsApi struct {
|
||||
AppID string
|
||||
// 窗口标题
|
||||
Title string
|
||||
// 窗口内容
|
||||
Content string
|
||||
// 窗口图标
|
||||
Icon string
|
||||
}
|
||||
|
||||
func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string) *AlertWindowsApi {
|
||||
return &AlertWindowsApi{
|
||||
AppID: AppID,
|
||||
Title: Title,
|
||||
Content: Content,
|
||||
Icon: Icon,
|
||||
}
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if getConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
||||
script := fmt.Sprintf(`display notification "%s" with title "%s"`, a.Content, a.Title)
|
||||
|
||||
cmd := exec.Command("osascript", "-e", script)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
32
backend/data/alert_darwin_api_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
|
||||
"github.com/go-toast/toast"
|
||||
)
|
||||
|
||||
// @Author 2lovecode
|
||||
// @Date 2025/02/06 17:50
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
func TestAlert(t *testing.T) {
|
||||
notification := toast.Notification{
|
||||
AppID: "go-stock",
|
||||
Title: "Hello, World!",
|
||||
Message: "This is a toast notification.",
|
||||
Icon: "../../build/appicon.png",
|
||||
Duration: "short",
|
||||
Audio: toast.Default,
|
||||
}
|
||||
err := notification.Push()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
53
backend/data/alert_windows_api.go
Normal file
@@ -0,0 +1,53 @@
|
||||
//go:build windows
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-toast/toast"
|
||||
"go-stock/backend/logger"
|
||||
)
|
||||
|
||||
// AlertWindowsApi @Author spark
|
||||
// @Date 2025/1/8 9:40
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type AlertWindowsApi struct {
|
||||
AppID string
|
||||
// 窗口标题
|
||||
Title string
|
||||
// 窗口内容
|
||||
Content string
|
||||
// 窗口图标
|
||||
Icon string
|
||||
}
|
||||
|
||||
func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string) *AlertWindowsApi {
|
||||
return &AlertWindowsApi{
|
||||
AppID: AppID,
|
||||
Title: Title,
|
||||
Content: Content,
|
||||
Icon: Icon,
|
||||
}
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if getConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
||||
notification := toast.Notification{
|
||||
AppID: a.AppID,
|
||||
Title: a.Title,
|
||||
Message: a.Content,
|
||||
Icon: a.Icon,
|
||||
Duration: "short",
|
||||
Audio: toast.Default,
|
||||
}
|
||||
err := notification.Push()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
30
backend/data/alert_windows_api_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
//go:build windows
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-toast/toast"
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/1/8 9:40
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestAlert(t *testing.T) {
|
||||
notification := toast.Notification{
|
||||
AppID: "go-stock",
|
||||
Title: "Hello, World!",
|
||||
Message: "This is a toast notification.",
|
||||
Icon: "../../build/appicon.png",
|
||||
Duration: "short",
|
||||
Audio: toast.Default,
|
||||
}
|
||||
err := notification.Push()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
225
backend/data/crawler_api.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/chromedp/chromedp"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/13 9:25
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
type CrawlerApi struct {
|
||||
crawlerCtx context.Context
|
||||
crawlerBaseInfo CrawlerBaseInfo
|
||||
}
|
||||
|
||||
func (c *CrawlerApi) NewTimeOutCrawler(timeout int, crawlerBaseInfo CrawlerBaseInfo) CrawlerApi {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
return c.NewCrawler(ctx, crawlerBaseInfo)
|
||||
}
|
||||
func (c *CrawlerApi) NewCrawler(ctx context.Context, crawlerBaseInfo CrawlerBaseInfo) CrawlerApi {
|
||||
return CrawlerApi{
|
||||
crawlerCtx: ctx,
|
||||
crawlerBaseInfo: crawlerBaseInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bool) {
|
||||
htmlContent := ""
|
||||
path := getConfig().BrowserPath
|
||||
logger.SugaredLogger.Infof("Browser path:%s", path)
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
c.crawlerCtx,
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.Flag("headless", headless),
|
||||
chromedp.Flag("blink-settings", "imagesEnabled=false"),
|
||||
chromedp.Flag("disable-javascript", false),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
|
||||
chromedp.Flag("disable-background-networking", true),
|
||||
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
|
||||
chromedp.Flag("disable-background-timer-throttling", true),
|
||||
chromedp.Flag("disable-backgrounding-occluded-windows", true),
|
||||
chromedp.Flag("disable-breakpad", true),
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true),
|
||||
chromedp.Flag("disable-default-apps", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
|
||||
chromedp.Flag("disable-hang-monitor", true),
|
||||
chromedp.Flag("disable-ipc-flooding-protection", true),
|
||||
chromedp.Flag("disable-popup-blocking", true),
|
||||
chromedp.Flag("disable-prompt-on-repost", true),
|
||||
chromedp.Flag("disable-renderer-backgrounding", true),
|
||||
chromedp.Flag("disable-sync", true),
|
||||
chromedp.Flag("force-color-profile", "srgb"),
|
||||
chromedp.Flag("metrics-recording-only", true),
|
||||
chromedp.Flag("safebrowsing-disable-auto-update", true),
|
||||
chromedp.Flag("enable-automation", true),
|
||||
chromedp.Flag("password-store", "basic"),
|
||||
chromedp.Flag("use-mock-keychain", true),
|
||||
)
|
||||
defer pcancel()
|
||||
ctx, cancel := chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
err := chromedp.Run(ctx, chromedp.Navigate(url),
|
||||
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
|
||||
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
|
||||
chromedp.InnerHTML("body", &htmlContent),
|
||||
)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false
|
||||
}
|
||||
} else {
|
||||
ctx, cancel := chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
err := chromedp.Run(ctx, chromedp.Navigate(url), chromedp.WaitVisible("body"), chromedp.InnerHTML("body", &htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return htmlContent, true
|
||||
|
||||
}
|
||||
|
||||
func (c *CrawlerApi) GetHtmlWithNoCancel(url, waitVisible string, headless bool) (html string, ok bool, parent context.CancelFunc, child context.CancelFunc) {
|
||||
htmlContent := ""
|
||||
path := getConfig().BrowserPath
|
||||
logger.SugaredLogger.Infof("BrowserPath :%s", path)
|
||||
var parentCancel context.CancelFunc
|
||||
var childCancel context.CancelFunc
|
||||
var pctx context.Context
|
||||
var cctx context.Context
|
||||
|
||||
if path != "" {
|
||||
pctx, parentCancel = chromedp.NewExecAllocator(
|
||||
c.crawlerCtx,
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.Flag("headless", headless),
|
||||
chromedp.Flag("blink-settings", "imagesEnabled=false"),
|
||||
chromedp.Flag("disable-javascript", false),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
|
||||
chromedp.Flag("disable-background-networking", true),
|
||||
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
|
||||
chromedp.Flag("disable-background-timer-throttling", true),
|
||||
chromedp.Flag("disable-backgrounding-occluded-windows", true),
|
||||
chromedp.Flag("disable-breakpad", true),
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true),
|
||||
chromedp.Flag("disable-default-apps", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
|
||||
chromedp.Flag("disable-hang-monitor", true),
|
||||
chromedp.Flag("disable-ipc-flooding-protection", true),
|
||||
chromedp.Flag("disable-popup-blocking", true),
|
||||
chromedp.Flag("disable-prompt-on-repost", true),
|
||||
chromedp.Flag("disable-renderer-backgrounding", true),
|
||||
chromedp.Flag("disable-sync", true),
|
||||
chromedp.Flag("force-color-profile", "srgb"),
|
||||
chromedp.Flag("metrics-recording-only", true),
|
||||
chromedp.Flag("safebrowsing-disable-auto-update", true),
|
||||
chromedp.Flag("enable-automation", true),
|
||||
chromedp.Flag("password-store", "basic"),
|
||||
chromedp.Flag("use-mock-keychain", true),
|
||||
)
|
||||
//defer pcancel()
|
||||
cctx, childCancel = chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
//defer cancel()
|
||||
err := chromedp.Run(cctx, chromedp.Navigate(url),
|
||||
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
|
||||
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
|
||||
chromedp.InnerHTML("body", &htmlContent),
|
||||
)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false, parentCancel, childCancel
|
||||
}
|
||||
} else {
|
||||
cctx, childCancel = chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
//defer cancel()
|
||||
err := chromedp.Run(cctx, chromedp.Navigate(url), chromedp.WaitVisible("body"), chromedp.InnerHTML("body", &htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false, parentCancel, childCancel
|
||||
}
|
||||
}
|
||||
return htmlContent, true, parentCancel, childCancel
|
||||
|
||||
}
|
||||
|
||||
func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless bool) (string, bool) {
|
||||
htmlContent := ""
|
||||
*actions = append(*actions, chromedp.InnerHTML("body", &htmlContent))
|
||||
|
||||
path := getConfig().BrowserPath
|
||||
logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
c.crawlerCtx,
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.Flag("headless", headless),
|
||||
chromedp.Flag("blink-settings", "imagesEnabled=false"),
|
||||
chromedp.Flag("disable-javascript", false),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
|
||||
chromedp.Flag("disable-background-networking", true),
|
||||
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
|
||||
chromedp.Flag("disable-background-timer-throttling", true),
|
||||
chromedp.Flag("disable-backgrounding-occluded-windows", true),
|
||||
chromedp.Flag("disable-breakpad", true),
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true),
|
||||
chromedp.Flag("disable-default-apps", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
|
||||
chromedp.Flag("disable-hang-monitor", true),
|
||||
chromedp.Flag("disable-ipc-flooding-protection", true),
|
||||
chromedp.Flag("disable-popup-blocking", true),
|
||||
chromedp.Flag("disable-prompt-on-repost", true),
|
||||
chromedp.Flag("disable-renderer-backgrounding", true),
|
||||
chromedp.Flag("disable-sync", true),
|
||||
chromedp.Flag("force-color-profile", "srgb"),
|
||||
chromedp.Flag("metrics-recording-only", true),
|
||||
chromedp.Flag("safebrowsing-disable-auto-update", true),
|
||||
chromedp.Flag("enable-automation", true),
|
||||
chromedp.Flag("password-store", "basic"),
|
||||
chromedp.Flag("use-mock-keychain", true),
|
||||
)
|
||||
defer pcancel()
|
||||
ctx, cancel := chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
|
||||
err := chromedp.Run(ctx, *actions...)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false
|
||||
}
|
||||
} else {
|
||||
ctx, cancel := chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
|
||||
err := chromedp.Run(ctx, *actions...)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
return htmlContent, true
|
||||
}
|
||||
|
||||
type CrawlerBaseInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
BaseUrl string `json:"base_url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
310
backend/data/crawler_api_test.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewTimeOutGuShiTongCrawler(t *testing.T) {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
timeout := 10
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://gushitong.baidu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
|
||||
result := crawlerAPI.NewTimeOutCrawler(timeout, crawlerBaseInfo)
|
||||
assert.NotNil(t, result.crawlerCtx)
|
||||
assert.Equal(t, crawlerBaseInfo, result.crawlerBaseInfo)
|
||||
}
|
||||
|
||||
func TestNewGuShiTongCrawler(t *testing.T) {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://gushitong.baidu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
|
||||
result := crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
assert.Equal(t, ctx, result.crawlerCtx)
|
||||
assert.Equal(t, crawlerBaseInfo, result.crawlerBaseInfo)
|
||||
}
|
||||
|
||||
func TestGetHtml(t *testing.T) {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://gushitong.baidu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
url := "https://www.cls.cn/searchPage?type=depth&keyword=%E6%96%B0%E5%B8%8C%E6%9C%9B"
|
||||
waitVisible := ".search-telegraph-list,.subject-interest-list"
|
||||
|
||||
//url = "https://gushitong.baidu.com/stock/ab-600745"
|
||||
//waitVisible = "div.news-item"
|
||||
htmlContent, success := crawlerAPI.GetHtml(url, waitVisible, true)
|
||||
if success {
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
var messages []string
|
||||
document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
messages = append(messages, text)
|
||||
logger.SugaredLogger.Infof("搜索到消息-%s: %s", "", text)
|
||||
})
|
||||
}
|
||||
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
|
||||
}
|
||||
|
||||
func TestGetHtmlWithActions(t *testing.T) {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, CrawlerBaseInfo{
|
||||
Name: "百度股市通",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://gushitong.baidu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
})
|
||||
actions := []chromedp.Action{
|
||||
chromedp.Navigate("https://gushitong.baidu.com/stock/ab-600745"),
|
||||
chromedp.WaitVisible("div.cos-tab"),
|
||||
chromedp.Click(".header div.cos-tab:nth-child(6)", chromedp.ByQuery),
|
||||
chromedp.ScrollIntoView("div.finance-container >div.row:nth-child(3)"),
|
||||
chromedp.WaitVisible("div.cos-tabs-header-container"),
|
||||
chromedp.Click(".page-content .cos-tabs-header-container .cos-tabs-header .cos-tab:nth-child(1)", chromedp.ByQuery),
|
||||
chromedp.WaitVisible(".page-content .finance-container .report-col-content", chromedp.ByQuery),
|
||||
chromedp.Click(".page-content .cos-tabs-header-container .cos-tabs-header .cos-tab:nth-child(4)", chromedp.ByQuery),
|
||||
chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight);`, nil),
|
||||
chromedp.Sleep(1 * time.Second),
|
||||
}
|
||||
htmlContent, success := crawlerAPI.GetHtmlWithActions(&actions, false)
|
||||
if success {
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
var messages []string
|
||||
document.Find("div.report-table-list-container,div.report-row").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(selection.Text(), false)
|
||||
messages = append(messages, text)
|
||||
logger.SugaredLogger.Infof("搜索到消息-%s: %s", "", text)
|
||||
})
|
||||
logger.SugaredLogger.Infof("messages:%d", len(messages))
|
||||
}
|
||||
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
|
||||
}
|
||||
|
||||
func TestHk(t *testing.T) {
|
||||
//https://stock.finance.sina.com.cn/hkstock/quotes/00001.html
|
||||
db.Init("../../data/stock.db")
|
||||
hks := &[]models.StockInfoHK{}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Limit(1).Find(hks)
|
||||
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://stock.finance.sina.com.cn",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
for _, hk := range *hks {
|
||||
logger.SugaredLogger.Infof("hk: %+v", hk)
|
||||
url := fmt.Sprintf("https://stock.finance.sina.com.cn/hkstock/quotes/%s.html", strings.ReplaceAll(hk.Code, ".HK", ""))
|
||||
htmlContent, ok := crawlerAPI.GetHtml(url, "#stock_cname", true)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
//logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
document.Find("#stock_cname").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
logger.SugaredLogger.Infof("股票名称-:%s", text)
|
||||
})
|
||||
|
||||
document.Find("#mts_stock_hk_price").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
logger.SugaredLogger.Infof("股票名称-现价: %s", text)
|
||||
})
|
||||
|
||||
document.Find(".deta_hqContainer >.deta03 li").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveNonPrintable(selection.Text())
|
||||
logger.SugaredLogger.Infof("股票名称-%s: %s", "", text)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUSName(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
us := &[]models.StockInfoUS{}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("name = ?", "").Order("RANDOM()").Find(us)
|
||||
|
||||
for _, us := range *us {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://stock.finance.sina.com.cn",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", us.Code[:len(us.Code)-3])
|
||||
logger.SugaredLogger.Infof("url: %s", url)
|
||||
//waitVisible := "span.quote_title_name"
|
||||
waitVisible := "div.hq_title > h1"
|
||||
|
||||
htmlContent, ok := crawlerAPI.GetHtml(url, waitVisible, true)
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
//logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
name := ""
|
||||
document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
|
||||
name = strutil.RemoveNonPrintable(selection.Text())
|
||||
name = strutil.SplitAndTrim(name, " ", "")[0]
|
||||
logger.SugaredLogger.Infof("股票名称-:%s", name)
|
||||
})
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", us.Code).Updates(map[string]interface{}{
|
||||
"name": name,
|
||||
"full_name": name,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
func TestUS(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
bytes, err := os.ReadFile("../../build/us.json")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://quote.eastmoney.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
tick := &Tick{}
|
||||
json.Unmarshal(bytes, &tick)
|
||||
for i, datum := range tick.Data {
|
||||
logger.SugaredLogger.Infof("datum: %d, %+v", i, datum)
|
||||
name := ""
|
||||
|
||||
//https://quote.eastmoney.com/us/AAPL.html
|
||||
//https://stock.finance.sina.com.cn/usstock/quotes/goog.html
|
||||
//url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", strings.ReplaceAll(datum.C, ".US", ""))
|
||||
////waitVisible := "span.quote_title_name"
|
||||
//waitVisible := "div.hq_title > h1"
|
||||
//
|
||||
//htmlContent, ok := crawlerAPI.GetHtml(url, waitVisible, true)
|
||||
//
|
||||
//if !ok {
|
||||
// continue
|
||||
//}
|
||||
////logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
|
||||
//document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Error(err.Error())
|
||||
//}
|
||||
//document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
|
||||
// name = strutil.RemoveNonPrintable(selection.Text())
|
||||
// name = strutil.SplitAndTrim(name, " ", "")[0]
|
||||
// logger.SugaredLogger.Infof("股票名称-:%s", name)
|
||||
//})
|
||||
|
||||
us := &models.StockInfoUS{
|
||||
Code: datum.C + ".US",
|
||||
EName: datum.N,
|
||||
FullName: datum.N,
|
||||
Name: name,
|
||||
Exchange: datum.E,
|
||||
Type: datum.T,
|
||||
}
|
||||
db.Dao.Create(us)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUSSINA(t *testing.T) {
|
||||
//https://finance.sina.com.cn/stock/usstock/sector.shtml#cm
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://quote.eastmoney.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
html, ok := crawlerAPI.GetHtml("https://finance.sina.com.cn/stock/usstock/sector.shtml#cm", "div#data", false)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
document.Find("div#data > table >tbody >tr").Each(func(i int, selection *goquery.Selection) {
|
||||
tr := selection.Text()
|
||||
logger.SugaredLogger.Infof("tr: %s", tr)
|
||||
})
|
||||
}
|
||||
|
||||
type Tick struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Data []struct {
|
||||
C string `json:"c"`
|
||||
N string `json:"n"`
|
||||
T string `json:"t"`
|
||||
E string `json:"e"`
|
||||
} `json:"data"`
|
||||
}
|
||||
85
backend/data/dingding_api.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/logger"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/1/3 13:53
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
type DingDingAPI struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewDingDingAPI() *DingDingAPI {
|
||||
return &DingDingAPI{
|
||||
client: resty.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
if getConfig().DingPushEnable == false {
|
||||
logger.SugaredLogger.Info("钉钉推送未开启")
|
||||
return "钉钉推送未开启"
|
||||
}
|
||||
// 发送钉钉消息
|
||||
resp, err := resty.New().R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(message).
|
||||
Post(getApiURL())
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "发送钉钉消息失败"
|
||||
}
|
||||
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
|
||||
return "发送钉钉消息成功"
|
||||
}
|
||||
func getConfig() *Settings {
|
||||
return NewSettingsApi(&Settings{}).GetConfig()
|
||||
}
|
||||
func getApiURL() string {
|
||||
return getConfig().DingRobot
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendToDingDing(title, message string) string {
|
||||
// 发送钉钉消息
|
||||
resp, err := resty.New().R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(&Message{
|
||||
Msgtype: "markdown",
|
||||
Markdown: Markdown{
|
||||
Title: "go-stock " + title,
|
||||
Text: message,
|
||||
},
|
||||
At: At{
|
||||
IsAtAll: true,
|
||||
},
|
||||
}).
|
||||
Post(getApiURL())
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "发送钉钉消息失败"
|
||||
}
|
||||
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
|
||||
return "发送钉钉消息成功"
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Msgtype string `json:"msgtype"`
|
||||
Markdown Markdown `json:"markdown"`
|
||||
At At `json:"at"`
|
||||
}
|
||||
|
||||
type Markdown struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type At struct {
|
||||
AtMobiles []string `json:"atMobiles"`
|
||||
AtUserIds []string `json:"atUserIds"`
|
||||
IsAtAll bool `json:"isAtAll"`
|
||||
}
|
||||
32
backend/data/dingding_api_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/1/3 13:53
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestRobot(t *testing.T) {
|
||||
dingdingRobotUrl := "XXX"
|
||||
resp, err := resty.New().R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(`{
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title":"go-stock",
|
||||
"text": "#### 杭州天气 @150XXXXXXXX \n > 9度,西北风1级,空气良89,相对温度73%\n > \n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n"
|
||||
},
|
||||
"at": {
|
||||
"isAtAll": true
|
||||
}
|
||||
}`).
|
||||
Post(dingdingRobotUrl)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Log(resp.String())
|
||||
}
|
||||
401
backend/data/fund_data_api.go
Normal file
@@ -0,0 +1,401 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FundApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
}
|
||||
|
||||
func NewFundApi() *FundApi {
|
||||
return &FundApi{
|
||||
client: resty.New(),
|
||||
config: getConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
type FollowedFund struct {
|
||||
gorm.Model
|
||||
Code string `json:"code" gorm:"index"` // 基金代码
|
||||
Name string `json:"name"` // 基金简称
|
||||
|
||||
NetUnitValue *float64 `json:"netUnitValue"` // 单位净值
|
||||
NetUnitValueDate string `json:"netUnitValueDate"` // 单位净值日期
|
||||
NetEstimatedUnit *float64 `json:"netEstimatedUnit"` // 估算单位净值
|
||||
NetEstimatedTime string `json:"netEstimatedUnitTime"` // 估算单位净值日期
|
||||
NetAccumulated *float64 `json:"netAccumulated"` // 累计净值
|
||||
|
||||
//计算值
|
||||
NetEstimatedRate *float64 `json:"netEstimatedRate"` // 估算单位净值涨跌幅
|
||||
|
||||
FundBasic FundBasic `json:"fundBasic" gorm:"foreignKey:Code;references:Code"`
|
||||
}
|
||||
|
||||
func (FollowedFund) TableName() string {
|
||||
return "followed_fund"
|
||||
}
|
||||
|
||||
// FundBasic 基金基本信息结构体
|
||||
type FundBasic struct {
|
||||
gorm.Model
|
||||
Code string `json:"code" gorm:"index"` // 基金代码
|
||||
Name string `json:"name"` // 基金简称
|
||||
FullName string `json:"fullName"` // 基金全称
|
||||
Type string `json:"type"` // 基金类型
|
||||
Establishment string `json:"establishment"` // 成立日期
|
||||
Scale string `json:"scale"` // 最新规模(亿元)
|
||||
Company string `json:"company"` // 基金管理人
|
||||
Manager string `json:"manager"` // 基金经理
|
||||
Rating string `json:"rating"` //基金评级
|
||||
TrackingTarget string `json:"trackingTarget"` //跟踪标的
|
||||
|
||||
NetUnitValue *float64 `json:"netUnitValue"` // 单位净值
|
||||
NetUnitValueDate string `json:"netUnitValueDate"` // 单位净值日期
|
||||
NetEstimatedUnit *float64 `json:"netEstimatedUnit"` // 估算单位净值
|
||||
NetEstimatedTime string `json:"netEstimatedUnitTime"` // 估算单位净值日期
|
||||
NetAccumulated *float64 `json:"netAccumulated"` // 累计净值
|
||||
|
||||
//净值涨跌幅: 近1月,近3月,近6月,近1年,近3年,近5年,今年来,成立来
|
||||
NetGrowth1 *float64 `json:"netGrowth1"` //近1月
|
||||
NetGrowth3 *float64 `json:"netGrowth3"` //近3月
|
||||
NetGrowth6 *float64 `json:"netGrowth6"` //近6月
|
||||
NetGrowth12 *float64 `json:"netGrowth12"` //近1年
|
||||
NetGrowth36 *float64 `json:"netGrowth36"` //近3年
|
||||
NetGrowth60 *float64 `json:"netGrowth60"` //近5年
|
||||
NetGrowthYTD *float64 `json:"netGrowthYTD"` //今年来
|
||||
NetGrowthAll *float64 `json:"netGrowthAll"` //成立来
|
||||
}
|
||||
|
||||
func (FundBasic) TableName() string {
|
||||
return "fund_basic"
|
||||
}
|
||||
|
||||
// CrawlFundBasic 爬取基金基本信息
|
||||
func (f *FundApi) CrawlFundBasic(fundCode string) (*FundBasic, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("CrawlFundBasic panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
crawler := CrawlerApi{
|
||||
crawlerBaseInfo: CrawlerBaseInfo{
|
||||
Name: "天天基金",
|
||||
BaseUrl: "http://fund.eastmoney.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(f.config.CrawlTimeOut)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
crawler = crawler.NewCrawler(ctx, crawler.crawlerBaseInfo)
|
||||
url := fmt.Sprintf("%s/%s.html", crawler.crawlerBaseInfo.BaseUrl, fundCode)
|
||||
logger.SugaredLogger.Infof("CrawlFundBasic url:%s", url)
|
||||
|
||||
// 使用现有爬虫框架解析页面
|
||||
htmlContent, ok := crawler.GetHtml(url, ".merchandiseDetail", true)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("页面解析失败")
|
||||
}
|
||||
|
||||
fund := &FundBasic{Code: fundCode}
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析基础信息
|
||||
name := doc.Find(".merchandiseDetail .fundDetail-tit").First().Text()
|
||||
fund.Name = strings.TrimSpace(strutil.ReplaceWithMap(name, map[string]string{"查看相关ETF>": ""}))
|
||||
logger.SugaredLogger.Infof("基金名称:%s", fund.Name)
|
||||
|
||||
doc.Find(".infoOfFund table td ").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(s.Text(), true)
|
||||
logger.SugaredLogger.Infof("基金信息:%+v", text)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
splitEx := strutil.SplitEx(text, ":", true)
|
||||
if strutil.ContainsAny(text, []string{"基金类型", "类型"}) {
|
||||
fund.Type = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"成立日期", "成立日"}) {
|
||||
fund.Establishment = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"基金规模", "规模"}) {
|
||||
fund.Scale = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"管理人", "基金公司"}) {
|
||||
fund.Company = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"基金经理", "经理人"}) {
|
||||
fund.Manager = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"基金评级", "评级"}) {
|
||||
fund.Rating = splitEx[1]
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"跟踪标的", "标的"}) {
|
||||
fund.TrackingTarget = splitEx[1]
|
||||
}
|
||||
})
|
||||
|
||||
//获取基金净值涨跌幅信息
|
||||
doc.Find(".dataOfFund dl > dd").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(s.Text(), true)
|
||||
logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", text)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
splitEx := strutil.SplitAndTrim(text, ":", "%")
|
||||
toFloat, err1 := convertor.ToFloat(splitEx[1])
|
||||
if err1 != nil {
|
||||
logger.SugaredLogger.Errorf("转换失败:%+v", err)
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", toFloat)
|
||||
if strutil.ContainsAny(text, []string{"近1月"}) {
|
||||
fund.NetGrowth1 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近3月"}) {
|
||||
fund.NetGrowth3 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近6月"}) {
|
||||
fund.NetGrowth6 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近1年"}) {
|
||||
fund.NetGrowth12 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近3年"}) {
|
||||
fund.NetGrowth36 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"近5年"}) {
|
||||
fund.NetGrowth60 = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"今年来"}) {
|
||||
fund.NetGrowthYTD = &toFloat
|
||||
}
|
||||
if strutil.ContainsAny(text, []string{"成立来"}) {
|
||||
fund.NetGrowthAll = &toFloat
|
||||
}
|
||||
})
|
||||
doc.Find(".dataOfFund dl > dd.dataNums,.dataOfFund dl > dt").Each(func(i int, s *goquery.Selection) {
|
||||
text := s.Text()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
logger.SugaredLogger.Infof("净值信息:%+v", text)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof("基金信息:%+v", fund)
|
||||
|
||||
count := int64(0)
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Count(&count)
|
||||
if count == 0 {
|
||||
db.Dao.Create(fund)
|
||||
} else {
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
|
||||
}
|
||||
|
||||
return fund, nil
|
||||
}
|
||||
|
||||
func (f *FundApi) GetFundList(key string) []FundBasic {
|
||||
var funds []FundBasic
|
||||
db.Dao.Where("code like ? or name like ?", "%"+key+"%", "%"+key+"%").Limit(10).Find(&funds)
|
||||
return funds
|
||||
}
|
||||
|
||||
func (f *FundApi) GetFollowedFund() []FollowedFund {
|
||||
var funds []FollowedFund
|
||||
db.Dao.Preload("FundBasic").Find(&funds)
|
||||
for i, fund := range funds {
|
||||
if fund.NetUnitValue != nil && fund.NetEstimatedUnit != nil && *fund.NetUnitValue > 0 {
|
||||
netEstimatedRate := (*(funds[i].NetEstimatedUnit) - *(funds[i].NetUnitValue)) / *(fund.NetUnitValue) * 100
|
||||
netEstimatedRate = mathutil.RoundToFloat(netEstimatedRate, 2)
|
||||
funds[i].NetEstimatedRate = &netEstimatedRate
|
||||
}
|
||||
|
||||
}
|
||||
return funds
|
||||
}
|
||||
func (f *FundApi) FollowFund(fundCode string) string {
|
||||
var fund FundBasic
|
||||
db.Dao.Where("code=?", fundCode).First(&fund)
|
||||
if fund.Code != "" {
|
||||
follow := &FollowedFund{
|
||||
Code: fundCode,
|
||||
Name: fund.Name,
|
||||
}
|
||||
err := db.Dao.Model(follow).Where("code = ?", fundCode).FirstOrCreate(follow, "code", fund.Code).Error
|
||||
if err != nil {
|
||||
return "关注失败"
|
||||
}
|
||||
return "关注成功"
|
||||
} else {
|
||||
return "基金信息不存在"
|
||||
}
|
||||
}
|
||||
func (f *FundApi) UnFollowFund(fundCode string) string {
|
||||
var fund FollowedFund
|
||||
db.Dao.Where("code=?", fundCode).First(&fund)
|
||||
if fund.Code != "" {
|
||||
err := db.Dao.Model(&fund).Delete(&fund).Error
|
||||
if err != nil {
|
||||
return "取消关注失败"
|
||||
}
|
||||
return "取消关注成功"
|
||||
} else {
|
||||
return "基金信息不存在"
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FundApi) AllFund() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("AllFund panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36").
|
||||
Get("https://fund.eastmoney.com/allfund.html")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//中文编码
|
||||
htmlContent := GB18030ToUTF8(response.Body())
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
cnt := 0
|
||||
doc.Find("ul.num_right li").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.SplitEx(s.Text(), "|", true)
|
||||
if len(text) > 0 {
|
||||
cnt++
|
||||
name := text[0]
|
||||
str := strutil.SplitAndTrim(name, ")", "(", ")")
|
||||
logger.SugaredLogger.Infof("%d,基金信息 code:%s,name:%s", cnt, str[0], str[1])
|
||||
//go f.CrawlFundBasic(str[0])
|
||||
fund := &FundBasic{
|
||||
Code: str[0],
|
||||
Name: str[1],
|
||||
}
|
||||
count := int64(0)
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Count(&count)
|
||||
if count == 0 {
|
||||
db.Dao.Create(fund)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type FundNetUnitValue struct {
|
||||
Fundcode string `json:"fundcode"`
|
||||
Name string `json:"name"`
|
||||
Jzrq string `json:"jzrq"`
|
||||
Dwjz string `json:"dwjz"`
|
||||
Gsz string `json:"gsz"`
|
||||
Gszzl string `json:"gszzl"`
|
||||
Gztime string `json:"gztime"`
|
||||
}
|
||||
|
||||
// CrawlFundNetEstimatedUnit 爬取净值估算值
|
||||
func (f *FundApi) CrawlFundNetEstimatedUnit(code string) {
|
||||
var fundNetUnitValue FundNetUnitValue
|
||||
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36").
|
||||
SetHeader("Referer", "https://fund.eastmoney.com/").
|
||||
SetQueryParams(map[string]string{"rt": strconv.FormatInt(time.Now().UnixMilli(), 10)}).
|
||||
Get(fmt.Sprintf("https://fundgz.1234567.com.cn/js/%s.js", code))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
if response.StatusCode() == 200 {
|
||||
htmlContent := string(response.Body())
|
||||
logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
|
||||
if strings.Contains(htmlContent, "jsonpgz") {
|
||||
htmlContent = strutil.Trim(htmlContent, "jsonpgz(", ");")
|
||||
htmlContent = strutil.Trim(htmlContent, ");")
|
||||
logger.SugaredLogger.Infof("基金净值信息:%s", htmlContent)
|
||||
err := json.Unmarshal([]byte(htmlContent), &fundNetUnitValue)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
fund := &FollowedFund{
|
||||
Code: fundNetUnitValue.Fundcode,
|
||||
Name: fundNetUnitValue.Name,
|
||||
NetEstimatedTime: fundNetUnitValue.Gztime,
|
||||
}
|
||||
netEstimatedUnit, err := convertor.ToFloat(fundNetUnitValue.Gsz)
|
||||
if err == nil {
|
||||
fund.NetEstimatedUnit = &netEstimatedUnit
|
||||
}
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CrawlFundNetUnitValue 爬取净值
|
||||
func (f *FundApi) CrawlFundNetUnitValue(code string) {
|
||||
// var fundNetUnitValue FundNetUnitValue
|
||||
url := fmt.Sprintf("http://hq.sinajs.cn/rn=%d&list=f_%s", time.Now().UnixMilli(), code)
|
||||
logger.SugaredLogger.Infof("url:%s", url)
|
||||
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "hq.sinajs.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
if response.StatusCode() == 200 {
|
||||
data := string(GB18030ToUTF8(response.Body()))
|
||||
logger.SugaredLogger.Infof("data:%s", data)
|
||||
datas := strutil.SplitAndTrim(data, "=", "\"")
|
||||
if len(datas) >= 2 {
|
||||
//codex := strings.Split(datas[0], "hq_str_f_")[1]
|
||||
parts := strutil.SplitAndTrim(datas[1], ",", "\"")
|
||||
logger.SugaredLogger.Infof("parts:%s", parts)
|
||||
val, err := convertor.ToFloat(parts[1])
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
fund := &FollowedFund{
|
||||
Name: parts[0],
|
||||
Code: code,
|
||||
NetUnitValue: &val,
|
||||
NetUnitValueDate: parts[4],
|
||||
}
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
23
backend/data/fund_data_api_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCrawlFundBasic(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
db.Dao.AutoMigrate(&FundBasic{})
|
||||
api := NewFundApi()
|
||||
|
||||
//api.CrawlFundBasic("510630")
|
||||
//api.CrawlFundBasic("159688")
|
||||
//
|
||||
api.AllFund()
|
||||
}
|
||||
|
||||
func TestCrawlFundNetUnitValue(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
api := NewFundApi()
|
||||
api.CrawlFundNetUnitValue("016533")
|
||||
}
|
||||
645
backend/data/openai_api.go
Normal file
@@ -0,0 +1,645 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/1/16 13:19
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type OpenAi struct {
|
||||
ctx context.Context
|
||||
BaseUrl string `json:"base_url"`
|
||||
ApiKey string `json:"api_key"`
|
||||
Model string `json:"model"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Prompt string `json:"prompt"`
|
||||
TimeOut int `json:"time_out"`
|
||||
QuestionTemplate string `json:"question_template"`
|
||||
CrawlTimeOut int64 `json:"crawl_time_out"`
|
||||
KDays int64 `json:"kDays"`
|
||||
BrowserPath string `json:"browser_path"`
|
||||
}
|
||||
|
||||
func NewDeepSeekOpenAi(ctx context.Context) *OpenAi {
|
||||
config := getConfig()
|
||||
if config.OpenAiEnable {
|
||||
if config.OpenAiApiTimeOut <= 0 {
|
||||
config.OpenAiApiTimeOut = 60 * 5
|
||||
}
|
||||
if config.CrawlTimeOut <= 0 {
|
||||
config.CrawlTimeOut = 60
|
||||
}
|
||||
if config.KDays < 30 {
|
||||
config.KDays = 120
|
||||
}
|
||||
}
|
||||
return &OpenAi{
|
||||
ctx: ctx,
|
||||
BaseUrl: config.OpenAiBaseUrl,
|
||||
ApiKey: config.OpenAiApiKey,
|
||||
Model: config.OpenAiModelName,
|
||||
MaxTokens: config.OpenAiMaxTokens,
|
||||
Temperature: config.OpenAiTemperature,
|
||||
Prompt: config.Prompt,
|
||||
TimeOut: config.OpenAiApiTimeOut,
|
||||
QuestionTemplate: config.QuestionTemplate,
|
||||
CrawlTimeOut: config.CrawlTimeOut,
|
||||
KDays: config.KDays,
|
||||
BrowserPath: config.BrowserPath,
|
||||
}
|
||||
}
|
||||
|
||||
type THSTokenResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type AiResponse struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Index int `json:"index"`
|
||||
Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
Logprobs interface{} `json:"logprobs"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
PromptCacheHitTokens int `json:"prompt_cache_hit_tokens"`
|
||||
PromptCacheMissTokens int `json:"prompt_cache_miss_tokens"`
|
||||
} `json:"usage"`
|
||||
SystemFingerprint string `json:"system_fingerprint"`
|
||||
}
|
||||
|
||||
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Error("NewChatStream panic", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Errorf("NewChatStream goroutine panic :%s", err)
|
||||
logger.SugaredLogger.Errorf("NewChatStream goroutine panic stock:%s stockCode:%s", stock, stockCode)
|
||||
logger.SugaredLogger.Errorf("NewChatStream goroutine panic config:%v", o)
|
||||
}
|
||||
}()
|
||||
defer close(ch)
|
||||
msg := []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
|
||||
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师,精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会,在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合,注重风险控制。\n\n【核心功能】\n\n市场分析维度:\n\n宏观经济(GDP/CPI/货币政策)\n\n行业景气度(产业链/政策红利/技术革新)\n\n个股三维诊断:\n\n基本面:PE/PB/ROE/现金流/护城河\n\n技术面:K线形态/均线系统/量价关系/指标背离\n\n资金面:主力动向/北向资金/融资余额/大宗交易\n\n智能策略库:\n√ 趋势跟踪策略(鳄鱼线+ADX)\n√ 波段交易策略(斐波那契回撤+RSI)\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系:\n▶ 动态止损:ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令(行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析:\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议(附压力测试结果)\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑(3点关键驱动力)\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时,采用苏格拉底式追问:\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%?\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式:\n📈 标的名称:XXXXXX\n⚖️ 多空信号:金叉确认/顶背离预警\n🎯 关键价位:支撑位XX.XX/压力位XX.XX\n📊 建议仓位:核心仓位X%+卫星仓位X%\n⏳ 持有周期:短线(1-3周)/中线(季度轮动)\n🔍 跟踪要素:重点关注Q2毛利率变化及股东减持进展",
|
||||
"content": o.Prompt,
|
||||
},
|
||||
}
|
||||
|
||||
question := ""
|
||||
if userQuestion == "" {
|
||||
replaceTemplates := map[string]string{
|
||||
"{{stockName}}": RemoveAllBlankChar(stock),
|
||||
"{{stockCode}}": RemoveAllBlankChar(stockCode),
|
||||
}
|
||||
|
||||
followedStock := &FollowedStock{
|
||||
StockCode: stockCode,
|
||||
}
|
||||
db.Dao.Model(&followedStock).Where("stock_code = ?", stockCode).First(followedStock)
|
||||
if followedStock.CostPrice > 0 {
|
||||
replaceTemplates["{{costPrice}}"] = fmt.Sprintf("%.2f", followedStock.CostPrice)
|
||||
}
|
||||
question = strutil.ReplaceWithMap(o.QuestionTemplate, replaceTemplates)
|
||||
} else {
|
||||
question = userQuestion
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("NewChatStream stock:%s stockCode:%s", stock, stockCode)
|
||||
logger.SugaredLogger.Infof("Prompt:%s", o.Prompt)
|
||||
logger.SugaredLogger.Infof("User Prompt config:%v", o.QuestionTemplate)
|
||||
logger.SugaredLogger.Infof("User question:%s", userQuestion)
|
||||
logger.SugaredLogger.Infof("final question:%s", question)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(6)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
endDate := time.Now().Format("20060102")
|
||||
startDate := time.Now().Add(-time.Hour * time.Duration(24*o.KDays)).Format("20060102")
|
||||
code := stockCode
|
||||
if strutil.HasPrefixAny(stockCode, []string{"hk", "sz", "sh"}) {
|
||||
code = ConvertStockCodeToTushareCode(stockCode)
|
||||
}
|
||||
K := NewTushareApi(getConfig()).GetDaily(code, startDate, endDate, o.CrawlTimeOut)
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + "日K数据如下:\n" + K,
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := SearchStockPriceInfo(stockCode, o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取股票价格失败")
|
||||
//ch <- "***❗获取股票价格失败,分析结果可能不准确***<hr>"
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"extraContent": "***❗获取股票价格失败,分析结果可能不准确***<hr>",
|
||||
}
|
||||
go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票价格失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
price := ""
|
||||
for _, message := range *messages {
|
||||
price += message + ";"
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + time.Now().Format(time.DateOnly) + "价格:" + price,
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if checkIsIndexBasic(stock) {
|
||||
return
|
||||
}
|
||||
|
||||
messages := GetFinancialReports(stockCode, o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取股票财报失败")
|
||||
// "***❗获取股票财报失败,分析结果可能不准确***<hr>"
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"extraContent": "***❗获取股票财报失败,分析结果可能不准确***<hr>",
|
||||
}
|
||||
go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票财报失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + message,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := GetTelegraphList(o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取市场资讯失败")
|
||||
//ch <- "***❗获取市场资讯失败,分析结果可能不准确***<hr>"
|
||||
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取市场资讯失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": message,
|
||||
})
|
||||
}
|
||||
messages = GetTopNewsList(o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取新闻资讯失败")
|
||||
//ch <- "***❗获取新闻资讯失败,分析结果可能不准确***<hr>"
|
||||
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取新闻资讯失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": message,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
//go func() {
|
||||
// defer wg.Done()
|
||||
// messages := SearchStockInfo(stock, "depth", o.CrawlTimeOut)
|
||||
// if messages == nil || len(*messages) == 0 {
|
||||
// logger.SugaredLogger.Error("获取股票资讯失败")
|
||||
// //ch <- "***❗获取股票资讯失败,分析结果可能不准确***<hr>"
|
||||
// //go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票资讯失败,分析结果可能不准确")
|
||||
// return
|
||||
// }
|
||||
// for _, message := range *messages {
|
||||
// msg = append(msg, map[string]interface{}{
|
||||
// "role": "user",
|
||||
// "content": message,
|
||||
// })
|
||||
// }
|
||||
//}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := SearchStockInfo(stock, "telegram", o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取股票电报资讯失败")
|
||||
//ch <- "***❗获取股票电报资讯失败,分析结果可能不准确***<hr>"
|
||||
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票电报资讯失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": message,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if checkIsIndexBasic(stock) {
|
||||
return
|
||||
}
|
||||
|
||||
messages := SearchGuShiTongStockInfo(stockCode, o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取股势通资讯失败")
|
||||
//ch <- "***❗获取股势通资讯失败,分析结果可能不准确***<hr>"
|
||||
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股势通资讯失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": message,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": question,
|
||||
})
|
||||
client := resty.New()
|
||||
client.SetBaseURL(strutil.Trim(o.BaseUrl))
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
//client.SetRetryCount(3)
|
||||
if o.TimeOut <= 0 {
|
||||
o.TimeOut = 300
|
||||
}
|
||||
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": o.Model,
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"stream": true,
|
||||
"messages": msg,
|
||||
}).
|
||||
Post("/chat/completions")
|
||||
|
||||
body := resp.RawBody()
|
||||
defer body.Close()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
logger.SugaredLogger.Infof("Received data: %s", line)
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
data := strutil.Trim(strings.TrimPrefix(line, "data:"))
|
||||
if data == "[DONE]" {
|
||||
return
|
||||
}
|
||||
|
||||
var streamResponse struct {
|
||||
Id string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content"`
|
||||
} `json:"delta"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
|
||||
for _, choice := range streamResponse.Choices {
|
||||
if content := choice.Delta.Content; content != "" {
|
||||
//ch <- content
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("Content data: %s", content)
|
||||
}
|
||||
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
|
||||
//ch <- reasoningContent
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": reasoningContent,
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
|
||||
}
|
||||
if choice.FinishReason == "stop" {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", data)
|
||||
//ch <- data
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": data,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strutil.RemoveNonPrintable(line) != "" {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", line)
|
||||
res := &models.Resp{}
|
||||
if err := json.Unmarshal([]byte(line), res); err == nil {
|
||||
//ch <- line
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": res.Message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func checkIsIndexBasic(stock string) bool {
|
||||
count := int64(0)
|
||||
db.Dao.Model(&IndexBasic{}).Where("name = ?", stock).Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func SearchGuShiTongStockInfo(stock string, crawlTimeOut int64) *[]string {
|
||||
crawlerAPI := CrawlerApi{}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, CrawlerBaseInfo{
|
||||
Name: "百度股市通",
|
||||
BaseUrl: "https://gushitong.baidu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
})
|
||||
url := "https://gushitong.baidu.com/stock/ab-" + RemoveAllNonDigitChar(stock)
|
||||
|
||||
if strutil.HasPrefixAny(stock, []string{"HK", "hk"}) {
|
||||
url = "https://gushitong.baidu.com/stock/hk-" + RemoveAllNonDigitChar(stock)
|
||||
}
|
||||
if strutil.HasPrefixAny(stock, []string{"SZ", "SH", "sh", "sz"}) {
|
||||
url = "https://gushitong.baidu.com/stock/ab-" + RemoveAllNonDigitChar(stock)
|
||||
}
|
||||
if strutil.HasPrefixAny(stock, []string{"us", "US", "gb_", "gb"}) {
|
||||
url = "https://gushitong.baidu.com/stock/us-" + strings.Replace(stock, "gb_", "", 1)
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索股票-%s: %s", stock, url)
|
||||
actions := []chromedp.Action{
|
||||
chromedp.Navigate(url),
|
||||
chromedp.WaitVisible("div.cos-tab"),
|
||||
chromedp.Click("div.cos-tab:nth-child(5)", chromedp.ByQuery),
|
||||
chromedp.ScrollIntoView("div.body-box"),
|
||||
chromedp.WaitVisible("div.body-col"),
|
||||
chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight);`, nil),
|
||||
chromedp.Sleep(1 * time.Second),
|
||||
}
|
||||
htmlContent, success := crawlerAPI.GetHtmlWithActions(&actions, true)
|
||||
var messages []string
|
||||
if success {
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]string{}
|
||||
}
|
||||
document.Find("div.finance-hover,div.list-date").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(selection.Text(), false)
|
||||
messages = append(messages, ReplaceSensitiveWords(text))
|
||||
logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索到消息-%s: %s", "", text)
|
||||
})
|
||||
logger.SugaredLogger.Infof("messages:%d", len(messages))
|
||||
}
|
||||
return &messages
|
||||
}
|
||||
|
||||
func GetFinancialReports(stockCode string, crawlTimeOut int64) *[]string {
|
||||
if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) {
|
||||
stockCode = strings.ReplaceAll(stockCode, "hk", "")
|
||||
stockCode = strings.ReplaceAll(stockCode, "HK", "")
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"us", "gb_"}) {
|
||||
stockCode = strings.ReplaceAll(stockCode, "us", "")
|
||||
stockCode = strings.ReplaceAll(stockCode, "gb_", "")
|
||||
}
|
||||
|
||||
// 创建一个 chromedp 上下文
|
||||
timeoutCtx, timeoutCtxCancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
|
||||
defer timeoutCtxCancel()
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
path := getConfig().BrowserPath
|
||||
logger.SugaredLogger.Infof("GetFinancialReports path:%s", path)
|
||||
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
timeoutCtx,
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.Flag("headless", true),
|
||||
chromedp.Flag("disable-javascript", false),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"),
|
||||
chromedp.Flag("disable-background-networking", true),
|
||||
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
|
||||
chromedp.Flag("disable-background-timer-throttling", true),
|
||||
chromedp.Flag("disable-backgrounding-occluded-windows", true),
|
||||
chromedp.Flag("disable-breakpad", true),
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true),
|
||||
chromedp.Flag("disable-default-apps", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
|
||||
chromedp.Flag("disable-hang-monitor", true),
|
||||
chromedp.Flag("disable-ipc-flooding-protection", true),
|
||||
chromedp.Flag("disable-popup-blocking", true),
|
||||
chromedp.Flag("disable-prompt-on-repost", true),
|
||||
chromedp.Flag("disable-renderer-backgrounding", true),
|
||||
chromedp.Flag("disable-sync", true),
|
||||
chromedp.Flag("force-color-profile", "srgb"),
|
||||
chromedp.Flag("metrics-recording-only", true),
|
||||
chromedp.Flag("safebrowsing-disable-auto-update", true),
|
||||
chromedp.Flag("enable-automation", true),
|
||||
chromedp.Flag("password-store", "basic"),
|
||||
chromedp.Flag("use-mock-keychain", true),
|
||||
)
|
||||
defer pcancel()
|
||||
ctx, cancel = chromedp.NewContext(
|
||||
pctx,
|
||||
chromedp.WithLogf(logger.SugaredLogger.Infof),
|
||||
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
|
||||
)
|
||||
} else {
|
||||
ctx, cancel = chromedp.NewContext(
|
||||
timeoutCtx,
|
||||
chromedp.WithLogf(logger.SugaredLogger.Infof),
|
||||
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
|
||||
)
|
||||
}
|
||||
defer cancel()
|
||||
var htmlContent string
|
||||
url := fmt.Sprintf("https://xueqiu.com/snowman/S/%s/detail#/ZYCWZB", stockCode)
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(url),
|
||||
// 等待页面加载完成,可以根据需要调整等待时间
|
||||
chromedp.WaitVisible("table.table", chromedp.ByQuery),
|
||||
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
|
||||
)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]string{}
|
||||
}
|
||||
var messages []string
|
||||
document.Find("table tr").Each(func(i int, selection *goquery.Selection) {
|
||||
tr := ""
|
||||
selection.Find("th,td").Each(func(i int, selection *goquery.Selection) {
|
||||
ret := selection.Find("p").First().Text()
|
||||
if ret == "" {
|
||||
ret = selection.Text()
|
||||
}
|
||||
text := strutil.RemoveNonPrintable(ret)
|
||||
tr += text + " "
|
||||
})
|
||||
logger.SugaredLogger.Infof("%s", tr+" \n")
|
||||
messages = append(messages, tr+" \n")
|
||||
})
|
||||
return &messages
|
||||
}
|
||||
|
||||
func GetTelegraphList(crawlTimeOut int64) *[]string {
|
||||
url := "https://www.cls.cn/telegraph"
|
||||
response, err := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(fmt.Sprintf(url))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
//logger.SugaredLogger.Info(string(response.Body()))
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
var telegraph []string
|
||||
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
|
||||
logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
|
||||
})
|
||||
return &telegraph
|
||||
}
|
||||
|
||||
func GetTopNewsList(crawlTimeOut int64) *[]string {
|
||||
url := "https://www.cls.cn"
|
||||
response, err := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(fmt.Sprintf(url))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
//logger.SugaredLogger.Info(string(response.Body()))
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
var telegraph []string
|
||||
document.Find("div.home-article-title a,div.home-article-rec a").Each(func(i int, selection *goquery.Selection) {
|
||||
logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
|
||||
})
|
||||
return &telegraph
|
||||
}
|
||||
|
||||
func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result, chatId, question string) {
|
||||
db.Dao.Create(&models.AIResponseResult{
|
||||
StockCode: stockCode,
|
||||
StockName: stockName,
|
||||
ModelName: o.Model,
|
||||
Content: result,
|
||||
ChatId: chatId,
|
||||
Question: question,
|
||||
})
|
||||
}
|
||||
|
||||
func (o OpenAi) GetAIResponseResult(stock string) *models.AIResponseResult {
|
||||
var result models.AIResponseResult
|
||||
db.Dao.Where("stock_code = ?", stock).Order("id desc").Limit(1).First(&result)
|
||||
return &result
|
||||
}
|
||||
30
backend/data/openai_api_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
//db.Init("../../data/stock.db")
|
||||
ai := NewDeepSeekOpenAi(context.TODO())
|
||||
res := ai.NewChatStream("上海贝岭", "sh600171", "分析以上股票资金流入信息,找出适合买入的股票,给出具体操作建议")
|
||||
for {
|
||||
select {
|
||||
case msg := <-res:
|
||||
t.Log(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTopNewsList(t *testing.T) {
|
||||
GetTopNewsList(30)
|
||||
}
|
||||
|
||||
func TestSearchGuShiTongStockInfo(t *testing.T) {
|
||||
//db.Init("../../data/stock.db")
|
||||
SearchGuShiTongStockInfo("hk01810", 60)
|
||||
SearchGuShiTongStockInfo("sh600745", 60)
|
||||
SearchGuShiTongStockInfo("gb_goog", 60)
|
||||
|
||||
}
|
||||
133
backend/data/settings_api.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
gorm.Model
|
||||
TushareToken string `json:"tushareToken"`
|
||||
LocalPushEnable bool `json:"localPushEnable"`
|
||||
DingPushEnable bool `json:"dingPushEnable"`
|
||||
DingRobot string `json:"dingRobot"`
|
||||
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
|
||||
RefreshInterval int64 `json:"refreshInterval"`
|
||||
|
||||
OpenAiEnable bool `json:"openAiEnable"`
|
||||
OpenAiBaseUrl string `json:"openAiBaseUrl"`
|
||||
OpenAiApiKey string `json:"openAiApiKey"`
|
||||
OpenAiModelName string `json:"openAiModelName"`
|
||||
OpenAiMaxTokens int `json:"openAiMaxTokens"`
|
||||
OpenAiTemperature float64 `json:"openAiTemperature"`
|
||||
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
|
||||
Prompt string `json:"prompt"`
|
||||
CheckUpdate bool `json:"checkUpdate"`
|
||||
QuestionTemplate string `json:"questionTemplate"`
|
||||
CrawlTimeOut int64 `json:"crawlTimeOut"`
|
||||
KDays int64 `json:"kDays"`
|
||||
EnableDanmu bool `json:"enableDanmu"`
|
||||
BrowserPath string `json:"browserPath"`
|
||||
EnableNews bool `json:"enableNews"`
|
||||
DarkTheme bool `json:"darkTheme"`
|
||||
}
|
||||
|
||||
func (receiver Settings) TableName() string {
|
||||
return "settings"
|
||||
}
|
||||
|
||||
type SettingsApi struct {
|
||||
Config Settings
|
||||
}
|
||||
|
||||
func NewSettingsApi(settings *Settings) *SettingsApi {
|
||||
return &SettingsApi{
|
||||
Config: *settings,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SettingsApi) UpdateConfig() string {
|
||||
count := int64(0)
|
||||
db.Dao.Model(s.Config).Count(&count)
|
||||
if count > 0 {
|
||||
db.Dao.Model(s.Config).Where("id=?", s.Config.ID).Updates(map[string]any{
|
||||
"local_push_enable": s.Config.LocalPushEnable,
|
||||
"ding_push_enable": s.Config.DingPushEnable,
|
||||
"ding_robot": s.Config.DingRobot,
|
||||
"update_basic_info_on_start": s.Config.UpdateBasicInfoOnStart,
|
||||
"refresh_interval": s.Config.RefreshInterval,
|
||||
"open_ai_enable": s.Config.OpenAiEnable,
|
||||
"open_ai_base_url": s.Config.OpenAiBaseUrl,
|
||||
"open_ai_api_key": s.Config.OpenAiApiKey,
|
||||
"open_ai_model_name": s.Config.OpenAiModelName,
|
||||
"open_ai_max_tokens": s.Config.OpenAiMaxTokens,
|
||||
"open_ai_temperature": s.Config.OpenAiTemperature,
|
||||
"tushare_token": s.Config.TushareToken,
|
||||
"prompt": s.Config.Prompt,
|
||||
"check_update": s.Config.CheckUpdate,
|
||||
"open_ai_api_time_out": s.Config.OpenAiApiTimeOut,
|
||||
"question_template": s.Config.QuestionTemplate,
|
||||
"crawl_time_out": s.Config.CrawlTimeOut,
|
||||
"k_days": s.Config.KDays,
|
||||
"enable_danmu": s.Config.EnableDanmu,
|
||||
"browser_path": s.Config.BrowserPath,
|
||||
"enable_news": s.Config.EnableNews,
|
||||
"dark_theme": s.Config.DarkTheme,
|
||||
})
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
|
||||
db.Dao.Model(s.Config).Create(&Settings{
|
||||
LocalPushEnable: s.Config.LocalPushEnable,
|
||||
DingPushEnable: s.Config.DingPushEnable,
|
||||
DingRobot: s.Config.DingRobot,
|
||||
UpdateBasicInfoOnStart: s.Config.UpdateBasicInfoOnStart,
|
||||
RefreshInterval: s.Config.RefreshInterval,
|
||||
OpenAiEnable: s.Config.OpenAiEnable,
|
||||
OpenAiBaseUrl: s.Config.OpenAiBaseUrl,
|
||||
OpenAiApiKey: s.Config.OpenAiApiKey,
|
||||
OpenAiModelName: s.Config.OpenAiModelName,
|
||||
OpenAiMaxTokens: s.Config.OpenAiMaxTokens,
|
||||
OpenAiTemperature: s.Config.OpenAiTemperature,
|
||||
TushareToken: s.Config.TushareToken,
|
||||
Prompt: s.Config.Prompt,
|
||||
CheckUpdate: s.Config.CheckUpdate,
|
||||
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
|
||||
QuestionTemplate: s.Config.QuestionTemplate,
|
||||
CrawlTimeOut: s.Config.CrawlTimeOut,
|
||||
KDays: s.Config.KDays,
|
||||
EnableDanmu: s.Config.EnableDanmu,
|
||||
BrowserPath: s.Config.BrowserPath,
|
||||
EnableNews: s.Config.EnableNews,
|
||||
DarkTheme: s.Config.DarkTheme,
|
||||
})
|
||||
}
|
||||
return "保存成功!"
|
||||
}
|
||||
func (s SettingsApi) GetConfig() *Settings {
|
||||
var settings Settings
|
||||
db.Dao.Model(&Settings{}).First(&settings)
|
||||
|
||||
if settings.OpenAiEnable {
|
||||
if settings.OpenAiApiTimeOut <= 0 {
|
||||
settings.OpenAiApiTimeOut = 60 * 5
|
||||
}
|
||||
if settings.CrawlTimeOut <= 0 {
|
||||
settings.CrawlTimeOut = 60
|
||||
}
|
||||
if settings.KDays < 30 {
|
||||
settings.KDays = 120
|
||||
}
|
||||
}
|
||||
if settings.BrowserPath == "" {
|
||||
settings.BrowserPath, _ = CheckBrowserOnWindows()
|
||||
}
|
||||
|
||||
return &settings
|
||||
}
|
||||
|
||||
func (s SettingsApi) Export() string {
|
||||
d, _ := json.MarshalIndent(s.GetConfig(), "", " ")
|
||||
return string(d)
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -14,9 +21,101 @@ import (
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestGetTelegraph(t *testing.T) {
|
||||
GetTelegraphList(30)
|
||||
}
|
||||
|
||||
func TestGetFinancialReports(t *testing.T) {
|
||||
//GetFinancialReports("sz000802", 30)
|
||||
//GetFinancialReports("hk00927", 30)
|
||||
GetFinancialReports("gb_aapl", 30)
|
||||
|
||||
}
|
||||
|
||||
func TestGetTelegraphSearch(t *testing.T) {
|
||||
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
|
||||
messages := SearchStockInfo("谷歌", "telegram", 30)
|
||||
for _, message := range *messages {
|
||||
logger.SugaredLogger.Info(message)
|
||||
}
|
||||
|
||||
//https://www.cls.cn/stock?code=sh600745
|
||||
}
|
||||
func TestSearchStockInfoByCode(t *testing.T) {
|
||||
SearchStockInfoByCode("sh600745")
|
||||
}
|
||||
|
||||
func TestSearchStockPriceInfo(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
//SearchStockPriceInfo("hk06030", 30)
|
||||
//SearchStockPriceInfo("sh600171", 30)
|
||||
//SearchStockPriceInfo("gb_aapl", 30)
|
||||
SearchStockPriceInfo("bj430198", 30)
|
||||
|
||||
}
|
||||
|
||||
func TestGetRealTimeStockPriceInfo(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
text, texttime := GetRealTimeStockPriceInfo(ctx, "sh600171")
|
||||
logger.SugaredLogger.Infof("res:%s,%s", text, texttime)
|
||||
|
||||
text, texttime = GetRealTimeStockPriceInfo(ctx, "sh600438")
|
||||
logger.SugaredLogger.Infof("res:%s,%s", text, texttime)
|
||||
|
||||
texttime = strings.ReplaceAll(texttime, ")", "")
|
||||
texttime = strings.ReplaceAll(texttime, "(", "")
|
||||
parts := strings.Split(texttime, " ")
|
||||
logger.SugaredLogger.Infof("parts:%+v", parts)
|
||||
|
||||
//去除中文字符
|
||||
// 正则表达式匹配中文字符
|
||||
re := regexp.MustCompile(`\p{Han}+`)
|
||||
texttime = re.ReplaceAllString(texttime, "")
|
||||
|
||||
logger.SugaredLogger.Infof("texttime:%s", texttime)
|
||||
location, err := time.ParseInLocation("2006-01-02 15:04:05", texttime, time.Local)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("location:%s", location.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
func TestParseFullSingleStockData(t *testing.T) {
|
||||
resp, err := resty.New().R().
|
||||
SetHeader("Host", "hq.sinajs.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600584,sz000938,hk01810,hk00856,gb_aapl,gb_tsla,sb873721,bj430300"))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
data := GB18030ToUTF8(resp.Body())
|
||||
strs := strutil.SplitEx(data, "\n", true)
|
||||
for _, str := range strs {
|
||||
logger.SugaredLogger.Info(str)
|
||||
stockData, err := ParseFullSingleStockData(str)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("%+#v", stockData)
|
||||
}
|
||||
|
||||
result, er := ParseFullSingleStockData("var hq_str_gb_tsla = \"特斯拉,268.8472,-5.55,2025-03-04 22:52:56,-15.8028,270.9300,278.2800,268.1000,488.5400,138.8030,23618295,88214389,864751599149,2.23,120.550000,0.00,0.00,0.00,0.00,3216517037,61,0.0000,0.00,0.00,,Mar 04 09:52AM EST,284.6500,0,1,2025,6458502467.0000,0.0000,0.0000,0.0000,0.0000,284.6500\";")
|
||||
if er != nil {
|
||||
logger.SugaredLogger.Error(er.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("%+#v", result)
|
||||
}
|
||||
|
||||
func TestNewStockDataApi(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
t.Log(stockDataApi.GetStockCodeRealTimeData("sh600859"))
|
||||
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla")
|
||||
for _, data := range *datas {
|
||||
t.Log(data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStockBaseInfo(t *testing.T) {
|
||||
@@ -74,6 +173,12 @@ func TestReadFile(t *testing.T) {
|
||||
func TestFollowedList(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
t.Log(stockDataApi.GetFollowList())
|
||||
stockDataApi.GetFollowList()
|
||||
|
||||
}
|
||||
|
||||
func TestStockDataApi_GetIndexBasic(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
stockDataApi.GetIndexBasic()
|
||||
}
|
||||
|
||||
93
backend/data/tushare_data_api.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/logger"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/17 12:33
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
type TushareApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
}
|
||||
|
||||
func NewTushareApi(config *Settings) *TushareApi {
|
||||
return &TushareApi{
|
||||
client: resty.New(),
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDaily tushare A股日线行情
|
||||
func (receiver TushareApi) GetDaily(tsCode, startDate, endDate string, crawlTimeOut int64) string {
|
||||
logger.SugaredLogger.Debugf("tushare daily request: ts_code=%s, start_date=%s, end_date=%s", tsCode, startDate, endDate)
|
||||
fields := "ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount"
|
||||
resp := &TushareStockBasicResponse{}
|
||||
stockType := getStockType(tsCode)
|
||||
tsCodeNEW := getTsCode(tsCode)
|
||||
logger.SugaredLogger.Debugf("tushare daily request: %s,tsCode:%s,tsCodeNEW:%s", stockType, tsCode, tsCodeNEW)
|
||||
_, err := receiver.client.SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("content-type", "application/json").
|
||||
SetBody(&TushareRequest{
|
||||
ApiName: stockType,
|
||||
Token: receiver.config.TushareToken,
|
||||
Params: map[string]any{
|
||||
"ts_code": tsCodeNEW,
|
||||
"start_date": startDate,
|
||||
"end_date": endDate,
|
||||
},
|
||||
Fields: fields}).
|
||||
SetResult(resp).
|
||||
Post(tushareApiUrl)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return ""
|
||||
}
|
||||
res := ""
|
||||
if resp.Data.Items != nil && len(resp.Data.Items) > 0 {
|
||||
fieldsStr := slice.JoinFunc(resp.Data.Fields, ",", func(s string) string {
|
||||
return "\"" + convertor.ToString(s) + "\""
|
||||
})
|
||||
res += fieldsStr + "\n"
|
||||
for _, item := range resp.Data.Items {
|
||||
//logger.SugaredLogger.Debugf("%s", slice.Join(item, ","))
|
||||
t := slice.JoinFunc(item, ",", func(s any) any {
|
||||
return "\"" + convertor.ToString(s) + "\""
|
||||
})
|
||||
res += t + "\n"
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Debugf("tushare response: %s", res)
|
||||
return res
|
||||
}
|
||||
|
||||
func getTsCode(code string) any {
|
||||
if strutil.HasPrefixAny(code, []string{"US", "us", "gb_"}) {
|
||||
code = strings.Replace(code, "gb_", "", 1)
|
||||
code = strings.Replace(code, "us", "", 1)
|
||||
return code
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
func getStockType(code string) string {
|
||||
if strutil.HasSuffixAny(code, []string{"SZ", "SH", "sh", "sz"}) {
|
||||
return "daily"
|
||||
}
|
||||
if strutil.HasSuffixAny(code, []string{"HK", "hk"}) {
|
||||
return "hk_daily"
|
||||
}
|
||||
if strutil.HasPrefixAny(code, []string{"US", "us", "gb_"}) {
|
||||
return "us_daily"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
29
backend/data/tushare_data_api_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/17 12:44
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
func TestGetDaily(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
tushareApi := NewTushareApi(getConfig())
|
||||
res := tushareApi.GetDaily("00927.HK", "20250101", "20250217", 30)
|
||||
t.Log(res)
|
||||
|
||||
}
|
||||
|
||||
func TestGetUSDaily(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
tushareApi := NewTushareApi(getConfig())
|
||||
|
||||
res := tushareApi.GetDaily("gb_AAPL", "20250101", "20250217", 30)
|
||||
t.Log(res)
|
||||
|
||||
//
|
||||
|
||||
}
|
||||
59
backend/data/utils.go
Normal file
47
backend/data/utils_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"go-stock/backend/logger"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRemoveNonPrintable tests the RemoveAllBlankChar function.
|
||||
func TestRemoveNonPrintable(t *testing.T) {
|
||||
//tests := []struct {
|
||||
// input string
|
||||
// expected string
|
||||
//}{
|
||||
// {"新 希 望", "新希望"},
|
||||
// {"", ""},
|
||||
// {"Hello, World!", "Hello, World!"},
|
||||
// {"\x00\x01\x02", ""},
|
||||
// {"Hello\x00World", "HelloWorld"},
|
||||
// {"\x1F\x20\x7E\x7F", " \x7E"},
|
||||
//}
|
||||
|
||||
//for _, test := range tests {
|
||||
// actual := RemoveAllBlankChar(test.input)
|
||||
// if actual != test.expected {
|
||||
// t.Errorf("RemoveAllBlankChar(%q) = %q; expected %q", test.input, actual, test.expected)
|
||||
// }
|
||||
//}
|
||||
txt := "新 希 望"
|
||||
txt2 := RemoveAllBlankChar(txt)
|
||||
logger.SugaredLogger.Infof("RemoveAllBlankChar(%s)", txt2)
|
||||
logger.SugaredLogger.Infof("RemoveAllBlankChar(%s)", txt)
|
||||
|
||||
}
|
||||
|
||||
func TestConvertStockCodeToTushareCode(t *testing.T) {
|
||||
logger.SugaredLogger.Infof("ConvertStockCodeToTushareCode(%s)", ConvertStockCodeToTushareCode("sz000802"))
|
||||
logger.SugaredLogger.Infof("ConvertTushareCodeToStockCode(%s)", ConvertTushareCodeToStockCode("000802.SZ"))
|
||||
}
|
||||
func TestReplaceSensitiveWords(t *testing.T) {
|
||||
txt := "新 希 望习近平"
|
||||
txt2 := ReplaceSensitiveWords(txt)
|
||||
logger.SugaredLogger.Infof("ReplaceSensitiveWords(%s)", txt2)
|
||||
|
||||
os.WriteFile("words.txt", []byte(slice.Join(SensitiveWords, "\n")), 0644)
|
||||
}
|
||||
197
backend/models/models.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/plugin/soft_delete"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/6 15:25
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
type GitHubReleaseVersion struct {
|
||||
Url string `json:"url"`
|
||||
AssetsUrl string `json:"assets_url"`
|
||||
UploadUrl string `json:"upload_url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
Id int `json:"id"`
|
||||
Author struct {
|
||||
Login string `json:"login"`
|
||||
Id int `json:"id"`
|
||||
NodeId string `json:"node_id"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
GravatarId string `json:"gravatar_id"`
|
||||
Url string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
FollowersUrl string `json:"followers_url"`
|
||||
FollowingUrl string `json:"following_url"`
|
||||
GistsUrl string `json:"gists_url"`
|
||||
StarredUrl string `json:"starred_url"`
|
||||
SubscriptionsUrl string `json:"subscriptions_url"`
|
||||
OrganizationsUrl string `json:"organizations_url"`
|
||||
ReposUrl string `json:"repos_url"`
|
||||
EventsUrl string `json:"events_url"`
|
||||
ReceivedEventsUrl string `json:"received_events_url"`
|
||||
Type string `json:"type"`
|
||||
UserViewType string `json:"user_view_type"`
|
||||
SiteAdmin bool `json:"site_admin"`
|
||||
} `json:"author"`
|
||||
NodeId string `json:"node_id"`
|
||||
TagName string `json:"tag_name"`
|
||||
TargetCommitish string `json:"target_commitish"`
|
||||
Name string `json:"name"`
|
||||
Draft bool `json:"draft"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
Assets []struct {
|
||||
Url string `json:"url"`
|
||||
Id int `json:"id"`
|
||||
NodeId string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Uploader struct {
|
||||
Login string `json:"login"`
|
||||
Id int `json:"id"`
|
||||
NodeId string `json:"node_id"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
GravatarId string `json:"gravatar_id"`
|
||||
Url string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
FollowersUrl string `json:"followers_url"`
|
||||
FollowingUrl string `json:"following_url"`
|
||||
GistsUrl string `json:"gists_url"`
|
||||
StarredUrl string `json:"starred_url"`
|
||||
SubscriptionsUrl string `json:"subscriptions_url"`
|
||||
OrganizationsUrl string `json:"organizations_url"`
|
||||
ReposUrl string `json:"repos_url"`
|
||||
EventsUrl string `json:"events_url"`
|
||||
ReceivedEventsUrl string `json:"received_events_url"`
|
||||
Type string `json:"type"`
|
||||
UserViewType string `json:"user_view_type"`
|
||||
SiteAdmin bool `json:"site_admin"`
|
||||
} `json:"uploader"`
|
||||
ContentType string `json:"content_type"`
|
||||
State string `json:"state"`
|
||||
Size int `json:"size"`
|
||||
DownloadCount int `json:"download_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
BrowserDownloadUrl string `json:"browser_download_url"`
|
||||
} `json:"assets"`
|
||||
TarballUrl string `json:"tarball_url"`
|
||||
ZipballUrl string `json:"zipball_url"`
|
||||
Body string `json:"body"`
|
||||
Tag Tag `json:"tag"`
|
||||
Commit Commit `json:"commit"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Ref string `json:"ref"`
|
||||
NodeId string `json:"node_id"`
|
||||
Url string `json:"url"`
|
||||
Object struct {
|
||||
Sha string `json:"sha"`
|
||||
Type string `json:"type"`
|
||||
Url string `json:"url"`
|
||||
} `json:"object"`
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Sha string `json:"sha"`
|
||||
NodeId string `json:"node_id"`
|
||||
Url string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
Author struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Date time.Time `json:"date"`
|
||||
} `json:"author"`
|
||||
Committer struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Date time.Time `json:"date"`
|
||||
} `json:"committer"`
|
||||
Tree struct {
|
||||
Sha string `json:"sha"`
|
||||
Url string `json:"url"`
|
||||
} `json:"tree"`
|
||||
Message string `json:"message"`
|
||||
Parents []struct {
|
||||
Sha string `json:"sha"`
|
||||
Url string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
} `json:"parents"`
|
||||
Verification struct {
|
||||
Verified bool `json:"verified"`
|
||||
Reason string `json:"reason"`
|
||||
Signature interface{} `json:"signature"`
|
||||
Payload interface{} `json:"payload"`
|
||||
VerifiedAt interface{} `json:"verified_at"`
|
||||
} `json:"verification"`
|
||||
}
|
||||
|
||||
type AIResponseResult struct {
|
||||
gorm.Model
|
||||
ChatId string `json:"chatId"`
|
||||
ModelName string `json:"modelName"`
|
||||
StockCode string `json:"stockCode"`
|
||||
StockName string `json:"stockName"`
|
||||
Question string `json:"question"`
|
||||
Content string `json:"content"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver AIResponseResult) TableName() string {
|
||||
return "ai_response_result"
|
||||
}
|
||||
|
||||
type VersionInfo struct {
|
||||
gorm.Model
|
||||
Version string `json:"version"`
|
||||
Content string `json:"content"`
|
||||
Icon string `json:"icon"`
|
||||
Alipay string `json:"alipay"`
|
||||
Wxpay string `json:"wxpay"`
|
||||
BuildTimeStamp int64 `json:"buildTimeStamp"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver VersionInfo) TableName() string {
|
||||
return "version_info"
|
||||
}
|
||||
|
||||
type StockInfoHK struct {
|
||||
gorm.Model
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"fullName"`
|
||||
EName string `json:"eName"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver StockInfoHK) TableName() string {
|
||||
return "stock_base_info_hk"
|
||||
}
|
||||
|
||||
type StockInfoUS struct {
|
||||
gorm.Model
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"fullName"`
|
||||
EName string `json:"eName"`
|
||||
Exchange string `json:"exchange"`
|
||||
Type string `json:"type"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver StockInfoUS) TableName() string {
|
||||
return "stock_base_info_us"
|
||||
}
|
||||
|
||||
type Resp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
49
backend/models/models_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/2/22 16:09
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type StockInfoHKResp struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
StockInfos *[]StockInfoData `json:"data"`
|
||||
}
|
||||
|
||||
type StockInfoData struct {
|
||||
C string `json:"c"`
|
||||
N string `json:"n"`
|
||||
T string `json:"t"`
|
||||
E string `json:"e"`
|
||||
}
|
||||
|
||||
func TestStockInfoHK(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
db.Dao.AutoMigrate(&StockInfoHK{})
|
||||
bs, _ := os.ReadFile("../../build/hk.json")
|
||||
v := &StockInfoHKResp{}
|
||||
err := json.Unmarshal(bs, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hks := &[]StockInfoHK{}
|
||||
for i, data := range *v.StockInfos {
|
||||
logger.SugaredLogger.Infof("第%d条数据: %+v", i, data)
|
||||
hk := &StockInfoHK{
|
||||
Code: strutil.PadStart(data.C, 5, "0") + ".HK",
|
||||
EName: data.N,
|
||||
}
|
||||
*hks = append(*hks, *hk)
|
||||
}
|
||||
db.Dao.Create(&hks)
|
||||
|
||||
}
|
||||
BIN
build/app.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
15192
build/hk.json
Normal file
BIN
build/screenshot/alipay.jpg
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
build/screenshot/img.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
build/screenshot/img_1.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
build/screenshot/img_10.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
build/screenshot/img_11.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
build/screenshot/img_2.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
build/screenshot/img_3.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
build/screenshot/img_4.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
build/screenshot/img_5.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
build/screenshot/img_6.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
build/screenshot/img_7.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
build/screenshot/img_8.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
build/screenshot/img_9.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
build/screenshot/wxpay.jpg
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
build/screenshot/扫码_搜索联合传播样式-白色版.png
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
27843
build/stock_base_info_hk.json
Normal file
36493
build/stock_base_info_us.json
Normal file
@@ -5,19 +5,19 @@
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "{{.Name}}"
|
||||
!define INFO_PROJECTNAME "go-stock"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||
!define INFO_COMPANYNAME "sparkmemory"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||
!define INFO_PRODUCTNAME "go-stock"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||
!define INFO_PRODUCTVERSION "1.0.0"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||
!define INFO_COPYRIGHT "Copyright#sparkmemory@163.com"
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
@@ -203,20 +203,12 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!macro wails.associateFiles
|
||||
; Create file associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
File "..\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateFiles
|
||||
; Delete app associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||
|
||||
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||
@@ -235,15 +227,10 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!macro wails.associateCustomProtocols
|
||||
; Create custom protocols associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
{{end}}
|
||||
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateCustomProtocols
|
||||
; Delete app custom protocol associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||
{{end}}
|
||||
|
||||
!macroend
|
||||
|
||||
BIN
build/wx.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>wails-naive-demo</title>
|
||||
<title>go-stock:AI赋能股票分析</title>
|
||||
<link href="./src/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
1294
frontend/package-lock.json
generated
@@ -9,14 +9,27 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.25"
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@vavt/cm-extension": "^1.8.0",
|
||||
"@vavt/v3-extension": "^3.0.0",
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"html2canvas": "^1.4.1",
|
||||
"md-editor-v3": "^5.2.3",
|
||||
"vue": "^3.2.25",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-danmaku": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"naive-ui": "^2.40.3",
|
||||
"html-docx-js-typescript": "^0.1.5",
|
||||
"naive-ui": "^2.41.0",
|
||||
"vfonts": "^0.0.3",
|
||||
"vite": "5.4.6"
|
||||
"vite": "^5.4.12"
|
||||
},
|
||||
"keywords": [],
|
||||
"keywords": [
|
||||
"AI赋能股票分析",
|
||||
"go-stock"
|
||||
],
|
||||
"author": "spark"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
9ce62efac1fed08499bbf20c8a5fd1b2
|
||||
2091cce83d29f564a50e85f1667b2f4c
|
||||
@@ -1,32 +1,286 @@
|
||||
<script setup>
|
||||
import stockInfo from './components/stock.vue'
|
||||
import {ref} from "vue";
|
||||
import {
|
||||
EventsEmit,
|
||||
EventsOn,
|
||||
Quit,
|
||||
WindowFullscreen, WindowGetPosition,
|
||||
WindowHide,
|
||||
WindowSetPosition,
|
||||
WindowUnfullscreen
|
||||
} from '../wailsjs/runtime'
|
||||
import {h, onBeforeMount, onMounted, ref} from "vue";
|
||||
import { RouterLink } from 'vue-router'
|
||||
import {darkTheme, NGradientText, NIcon, NText,} from 'naive-ui'
|
||||
import {
|
||||
SettingsOutline,
|
||||
ReorderTwoOutline,
|
||||
ExpandOutline,
|
||||
PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline, AlarmOutline, SparklesOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import {GetConfig} from "../wailsjs/go/main/App";
|
||||
const enableNews= ref(false)
|
||||
const contentStyle = ref("")
|
||||
const enableDarkTheme = ref(true)
|
||||
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
|
||||
const isFullscreen = ref(false)
|
||||
const activeKey = ref('')
|
||||
const containerRef= ref({})
|
||||
const realtimeProfit= ref(0)
|
||||
const telegraph= ref([])
|
||||
const menuOptions = ref([
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'stock',
|
||||
params: {
|
||||
},
|
||||
}
|
||||
},
|
||||
{ default: () => '股票自选',}
|
||||
),
|
||||
key: 'stock',
|
||||
icon: renderIcon(StarOutline),
|
||||
children:[
|
||||
{
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '当日盈亏 '+realtimeProfit.value+"¥"}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(WalletOutline),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'fund',
|
||||
params: {
|
||||
},
|
||||
}
|
||||
},
|
||||
{ default: () => '基金自选',}
|
||||
),
|
||||
key: 'fund',
|
||||
icon: renderIcon(SparklesOutline),
|
||||
children:[
|
||||
{
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '功能完善中!'}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(AlarmOutline),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'settings',
|
||||
params: {
|
||||
}
|
||||
}
|
||||
},
|
||||
{ default: () => '设置' }
|
||||
),
|
||||
key: 'settings',
|
||||
icon: renderIcon(SettingsOutline),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'about',
|
||||
params: {
|
||||
}
|
||||
}
|
||||
},
|
||||
{ default: () => '关于' }
|
||||
),
|
||||
key: 'about',
|
||||
icon: renderIcon(LogoGithub),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
href: '#',
|
||||
onClick: toggleFullscreen,
|
||||
title: '全屏 Ctrl+F 退出全屏 Esc',
|
||||
}, { default: () => isFullscreen.value?'取消全屏':'全屏' }),
|
||||
key: 'full',
|
||||
icon: renderIcon(ExpandOutline),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
href: '#',
|
||||
onClick: WindowHide,
|
||||
title: '隐藏到托盘区 Ctrl+H',
|
||||
}, { default: () => '隐藏到托盘区' }),
|
||||
key: 'hide',
|
||||
icon: renderIcon(ReorderTwoOutline),
|
||||
},
|
||||
// {
|
||||
// label: ()=> h("a", {
|
||||
// href: 'javascript:void(0)',
|
||||
// style: 'cursor: move;',
|
||||
// onClick: toggleStartMoveWindow,
|
||||
// }, { default: () => '移动' }),
|
||||
// key: 'move',
|
||||
// icon: renderIcon(MoveOutline),
|
||||
// },
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
href: '#',
|
||||
onClick: Quit,
|
||||
}, { default: () => '退出程序' }),
|
||||
key: 'exit',
|
||||
icon: renderIcon(PowerOutline),
|
||||
},
|
||||
])
|
||||
function renderIcon(icon) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) })
|
||||
}
|
||||
function toggleFullscreen(e) {
|
||||
//console.log(e)
|
||||
if (isFullscreen.value) {
|
||||
WindowUnfullscreen()
|
||||
//e.target.innerHTML = '全屏'
|
||||
} else {
|
||||
WindowFullscreen()
|
||||
// e.target.innerHTML = '取消全屏'
|
||||
}
|
||||
isFullscreen.value=!isFullscreen.value
|
||||
}
|
||||
// const drag = ref(false)
|
||||
// const lastPos= ref({x:0,y:0})
|
||||
// function toggleStartMoveWindow(e) {
|
||||
// drag.value=!drag.value
|
||||
// lastPos.value={x:e.clientX,y:e.clientY}
|
||||
// }
|
||||
// function dragstart(e) {
|
||||
// if (drag.value) {
|
||||
// let x=e.clientX-lastPos.value.x
|
||||
// let y=e.clientY-lastPos.value.y
|
||||
// WindowGetPosition().then((pos) => {
|
||||
// WindowSetPosition(pos.x+x,pos.y+y)
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// window.addEventListener('mousemove', dragstart)
|
||||
|
||||
EventsOn("realtime_profit",(data)=>{
|
||||
realtimeProfit.value=data
|
||||
})
|
||||
EventsOn("telegraph",(data)=>{
|
||||
telegraph.value=data
|
||||
})
|
||||
|
||||
window.onerror = function (msg, source, lineno, colno, error) {
|
||||
// 将错误信息发送给后端
|
||||
EventsEmit("frontendError", {
|
||||
page: "App.vue",
|
||||
message: msg,
|
||||
source: source,
|
||||
lineno: lineno,
|
||||
colno: colno,
|
||||
error: error ? error.stack : null,
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
onBeforeMount(()=>{
|
||||
// GetConfig().then((res)=>{
|
||||
// console.log(res)
|
||||
// if(res.darkTheme){
|
||||
// enableDarkTheme.value=darkTheme
|
||||
// }
|
||||
// if(res.enableNews){
|
||||
// enableNews.value=true
|
||||
// }
|
||||
// })
|
||||
})
|
||||
|
||||
onMounted(()=>{
|
||||
contentStyle.value="max-height: calc(90vh);overflow: hidden"
|
||||
GetConfig().then((res)=>{
|
||||
console.log(res)
|
||||
if(res.darkTheme){
|
||||
enableDarkTheme.value=darkTheme
|
||||
}else{
|
||||
enableDarkTheme.value=null
|
||||
}
|
||||
if(res.enableNews){
|
||||
enableNews.value=true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const content = ref('数据来源于网络,仅供参考\n投资有风险,入市需谨慎')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-watermark
|
||||
<n-config-provider ref="containerRef" :theme="enableDarkTheme" >
|
||||
<n-message-provider >
|
||||
<n-notification-provider>
|
||||
<n-modal-provider>
|
||||
<n-dialog-provider>
|
||||
|
||||
<n-watermark
|
||||
:content="content"
|
||||
cross
|
||||
selectable
|
||||
:font-size="12"
|
||||
:line-height="12"
|
||||
:font-size="16"
|
||||
:line-height="16"
|
||||
:width="500"
|
||||
:height="400"
|
||||
:width="200"
|
||||
:x-offset="50"
|
||||
:y-offset="50"
|
||||
:y-offset="150"
|
||||
:rotate="-15"
|
||||
style="height: 100%"
|
||||
>
|
||||
<n-flex justify="center">
|
||||
<n-message-provider >
|
||||
<n-modal-provider>
|
||||
<stockInfo/>
|
||||
</n-modal-provider>
|
||||
</n-message-provider>
|
||||
<n-grid x-gap="12" :cols="1">
|
||||
<!--
|
||||
<n-gi style="position: relative;top:1px;z-index: 19;width: 100%" v-if="telegraph.length>0">
|
||||
|
||||
</n-gi>
|
||||
-->
|
||||
|
||||
<n-gi>
|
||||
<n-marquee :speed="100" style="position: relative;top:0;z-index: 19;width: 100%" v-if="(telegraph.length>0)&&(enableNews)">
|
||||
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
|
||||
{{item}}
|
||||
</n-tag>
|
||||
<!-- <n-text type="warning"> {{telegraph[0]}}</n-text>-->
|
||||
</n-marquee>
|
||||
<n-scrollbar :style="contentStyle">
|
||||
<RouterView />
|
||||
</n-scrollbar>
|
||||
</n-gi>
|
||||
|
||||
|
||||
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
|
||||
<n-card size="small" style="--wails-draggable:drag">
|
||||
<n-menu style="font-size: 18px;"
|
||||
v-model:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
responsive
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
</n-watermark>
|
||||
</n-dialog-provider>
|
||||
</n-modal-provider>
|
||||
</n-notification-provider>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
<style>
|
||||
|
||||
|
||||
217
frontend/src/components/about.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<script setup>
|
||||
// import { MdPreview } from 'md-editor-v3';
|
||||
// preview.css相比style.css少了编辑器那部分样式
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
import {h, onMounted, ref} from 'vue';
|
||||
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
|
||||
import {EventsOn} from "../../wailsjs/runtime";
|
||||
import {NAvatar, NButton, useNotification} from "naive-ui";
|
||||
const updateLog = ref('');
|
||||
const versionInfo = ref('');
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
const alipay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/alipay.jpg')
|
||||
const wxpay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/wxpay.jpg')
|
||||
const notify = useNotification()
|
||||
|
||||
onMounted(() => {
|
||||
document.title = '关于软件';
|
||||
GetVersionInfo().then((res) => {
|
||||
updateLog.value = res.content;
|
||||
versionInfo.value = res.version;
|
||||
icon.value = res.icon;
|
||||
alipay.value=res.alipay;
|
||||
wxpay.value=res.wxpay;
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
||||
EventsOn("updateVersion",async (msg) => {
|
||||
const githubTimeStr = msg.published_at;
|
||||
// 创建一个 Date 对象
|
||||
const utcDate = new Date(githubTimeStr);
|
||||
// 获取本地时间
|
||||
const date = new Date(utcDate.getTime());
|
||||
const year = date.getFullYear();
|
||||
// getMonth 返回值是 0 - 11,所以要加 1
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
|
||||
console.log("GitHub UTC 时间:", utcDate);
|
||||
console.log("转换后的本地时间:", formattedDate);
|
||||
notify.info({
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
size: 'small',
|
||||
round: false,
|
||||
src: icon.value
|
||||
}),
|
||||
title: '发现新版本: ' + msg.tag_name,
|
||||
content: () => {
|
||||
//return h(MdPreview, {theme:'dark',modelValue:msg.commit?.message}, null)
|
||||
return h('div', {
|
||||
style: {
|
||||
'text-align': 'left',
|
||||
'font-size': '14px',
|
||||
}
|
||||
}, { default: () => msg.commit?.message })
|
||||
},
|
||||
duration: 5000,
|
||||
meta: "发布时间:"+formattedDate,
|
||||
action: () => {
|
||||
return h(NButton, {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
window.open(msg.html_url)
|
||||
}
|
||||
}, { default: () => '查看' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-space vertical size="large">
|
||||
<!-- 软件描述 -->
|
||||
<n-card size="large">
|
||||
<n-divider title-placement="center">关于软件</n-divider>
|
||||
<n-space vertical >
|
||||
<n-image width="100" :src="icon" />
|
||||
<h1>
|
||||
<n-badge :value="versionInfo" :offset="[50,10]" type="success">
|
||||
<n-gradient-text type="info" :size="50" >go-stock</n-gradient-text>
|
||||
</n-badge>
|
||||
</h1>
|
||||
<n-button size="tiny" @click="CheckUpdate" type="info" tertiary >检查更新</n-button>
|
||||
|
||||
|
||||
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
<p>自选股行情实时监控,基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
|
||||
<p>目前已支持A股,港股,美股,未来计划加入基金,ETF等支持</p>
|
||||
<p>支持DeepSeek,OpenAI, Ollama,LMStudio,AnythingLLM,<a href="https://cloud.siliconflow.cn/i/foufCerk" target="_blank">硅基流动</a>,<a href="https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ" target="_blank">火山方舟</a>,阿里云百炼等平台或模型</p>
|
||||
<p>
|
||||
<i style="color: crimson">本软件仅供学习研究目的,AI分析结果仅供参考,本软件不提供任何投资建议或决策,风险自担!</i>
|
||||
</p>
|
||||
<p>
|
||||
欢迎点赞GitHub:<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">go-stock</a><n-divider vertical />
|
||||
<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">GitHub</a><n-divider vertical />
|
||||
<a href="https://github.com/ArvinLovegood/go-stock/issues" target="_blank">Issues</a><n-divider vertical />
|
||||
<a href="https://github.com/ArvinLovegood/go-stock/releases" target="_blank">Releases</a><n-divider vertical />
|
||||
</p>
|
||||
<p v-if="updateLog">更新说明:{{updateLog}}</p>
|
||||
<p>项目社区:<a href="https://go-stock.sparkmemory.top/" target="_blank">https://go-stock.sparkmemory.top/</a></p>
|
||||
<p>QQ交流群:<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333" target="_blank">491605333</a></p>
|
||||
</div>
|
||||
</n-space>
|
||||
<n-divider title-placement="center">关于作者</n-divider>
|
||||
<n-space vertical>
|
||||
<!-- <h1>关于作者</h1>-->
|
||||
<n-avatar width="100" src="https://avatars.githubusercontent.com/u/7401917?v=4" />
|
||||
<h2><a href="https://github.com/ArvinLovegood" target="_blank">@ArvinLovegood</a></h2>
|
||||
<p>一个热爱编程的小白,欢迎关注我的Github</p>
|
||||
<n-image width="300" src="https://go-stock.sparkmemory.top/assets/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88-DEJtWc_y.png" />
|
||||
|
||||
<p>开源不易,如果觉得好用,可以请作者喝杯咖啡。</p>
|
||||
<n-flex justify="center">
|
||||
<n-image width="200" :src="alipay" />
|
||||
<n-image width="200" :src="wxpay" />
|
||||
</n-flex>
|
||||
</n-space>
|
||||
<n-divider title-placement="center">鸣谢</n-divider>
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
<p>
|
||||
感谢以下捐赠者:
|
||||
<n-gradient-text size="small" type="warning">*晨</n-gradient-text><n-divider vertical />
|
||||
</p>
|
||||
<p>
|
||||
感谢以下开发者:
|
||||
<a href="https://github.com/gnim2600" target="_blank">@gnim2600</a><n-divider vertical />
|
||||
<a href="https://github.com/XXXiaohuayanGGG" target="_blank">@XXXiaohuayanGGG</a><n-divider vertical />
|
||||
<a href="https://github.com/2lovecode" target="_blank">@2lovecode</a><n-divider vertical />
|
||||
<a href="https://github.com/JerryLookupU" target="_blank">@JerryLookupU</a><n-divider vertical />
|
||||
</p>
|
||||
<p>
|
||||
感谢以下开源项目:
|
||||
<a href="https://github.com/wailsapp/wails" target="_blank">Wails</a><n-divider vertical />
|
||||
<a href="https://github.com/vuejs" target="_blank">Vue</a><n-divider vertical />
|
||||
<a href="https://github.com/tusen-ai/naive-ui" target="_blank">NaiveUI</a><n-divider vertical />
|
||||
</p>
|
||||
</div>
|
||||
<n-divider title-placement="center">关于版权和技术支持申明</n-divider>
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
<p style="color: #FAA04A">如有问题,请先查看项目文档,如果问题依然存在,请优先加群(491605333)咨询。</p>
|
||||
<p>
|
||||
如需软件商业授权或定制开发,请联系作者微信(备注 商业咨询):ArvinLovegood
|
||||
</p>
|
||||
<n-divider/>
|
||||
<p>
|
||||
本软件基于开源技术构建,使用Wails、NaiveUI、Vue等开源项目。技术上如有问题,可以先向对应的开源社区请求帮助。
|
||||
</p>
|
||||
<p>
|
||||
开源不易,本人精力和时间有限,如确实需要一对一技术支持,<i style="color: crimson">请先赞助!</i>联系微信(备注 技术支持):ArvinLovegood
|
||||
</p>
|
||||
<p style="color: #FAA04A">*加微信或者QQ时,请先备注或留言需求(如:<a href="#support">技术支持</a>,功能建议,商业咨询等,否则会被忽略)</p>
|
||||
<n-table id="support">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>技术支持方式</n-th><n-th>赞助(元)</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr>
|
||||
<n-td>
|
||||
加 QQ:506808970,微信:ArvinLovegood
|
||||
</n-td>
|
||||
<n-td>
|
||||
100/次
|
||||
</n-td>
|
||||
</n-tr>
|
||||
<n-tr>
|
||||
<n-td>
|
||||
长期技术支持(不限次数,新功能优先体验等)
|
||||
</n-td>
|
||||
<n-td>
|
||||
5000
|
||||
</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</div>
|
||||
|
||||
</n-card>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 可以在这里添加一些样式 */
|
||||
h1, h2 {
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #18a058;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
277
frontend/src/components/fund.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<script setup>
|
||||
import {h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from "vue";
|
||||
import {Add, ChatboxOutline} from "@vicons/ionicons5";
|
||||
import {NButton, NEllipsis, NText, useMessage} from "naive-ui";
|
||||
import {
|
||||
FollowFund,
|
||||
GetConfig,
|
||||
GetFollowedFund,
|
||||
GetfundList,
|
||||
GetVersionInfo,
|
||||
UnFollowFund
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import vueDanmaku from 'vue3-danmaku'
|
||||
|
||||
const danmus = ref([])
|
||||
const ws = ref(null)
|
||||
const icon = ref(null)
|
||||
const message = useMessage()
|
||||
const modalShow = ref(false)
|
||||
const data = reactive({
|
||||
modelName:"",
|
||||
chatId: "",
|
||||
question:"",
|
||||
name: "",
|
||||
code: "",
|
||||
fenshiURL:"",
|
||||
kURL:"",
|
||||
fullscreen: false,
|
||||
airesult: "",
|
||||
openAiEnable: false,
|
||||
loading: true,
|
||||
enableDanmu: false,
|
||||
})
|
||||
|
||||
const followList=ref([])
|
||||
const options=ref([])
|
||||
const ticker=ref({})
|
||||
|
||||
onBeforeMount(()=>{
|
||||
GetConfig().then(result => {
|
||||
if (result.openAiEnable) {
|
||||
data.openAiEnable = true
|
||||
}
|
||||
if (result.enableDanmu) {
|
||||
data.enableDanmu = true
|
||||
}
|
||||
})
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
GetVersionInfo().then((res) => {
|
||||
icon.value = res.icon;
|
||||
});
|
||||
// 创建 WebSocket 连接
|
||||
ws.value = new WebSocket('ws://8.134.249.145:16688/ws'); // 替换为你的 WebSocket 服务器地址
|
||||
//ws.value = new WebSocket('ws://localhost:16688/ws'); // 替换为你的 WebSocket 服务器地址
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('WebSocket 连接已打开');
|
||||
};
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
if(data.enableDanmu){
|
||||
danmus.value.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
console.error('WebSocket 错误:', error);
|
||||
};
|
||||
|
||||
ws.value.onclose = () => {
|
||||
console.log('WebSocket 连接已关闭');
|
||||
};
|
||||
|
||||
ticker.value=setInterval(() => {
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
})
|
||||
}, 1000*60)
|
||||
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(ticker.value)
|
||||
ws.value.close()
|
||||
})
|
||||
|
||||
|
||||
|
||||
function SendDanmu(){
|
||||
ws.value.send(data.name)
|
||||
}
|
||||
function AddFund(){
|
||||
FollowFund(data.code).then(result=>{
|
||||
if(result){
|
||||
message.success("关注成功")
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
function unFollow(code){
|
||||
UnFollowFund(code).then(result=>{
|
||||
if(result){
|
||||
message.success("取消关注成功")
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getFundList(value){
|
||||
GetfundList(value).then(result=>{
|
||||
options.value=[]
|
||||
result.forEach(item=>{
|
||||
options.value.push({
|
||||
label: item.name+" ["+item.code+"]",
|
||||
value: item.code,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
function onSelectFund(value){
|
||||
data.code=value
|
||||
blinkBorder(value)
|
||||
}
|
||||
function formatterTitle(title){
|
||||
return () => h(NEllipsis,{
|
||||
style: {
|
||||
'font-size': '16px',
|
||||
'max-width': '180px',
|
||||
},
|
||||
},{default: () => title,}
|
||||
)
|
||||
}
|
||||
|
||||
function search(code,name){
|
||||
setTimeout(() => {
|
||||
window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
|
||||
//window.open("https://finance.sina.com.cn/fund/quotes/"+code+"/bc.shtml","_blank","width=1000,height=800,top=100,left=100,toolbar=no,location=no")
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function newchart(code,name){
|
||||
modalShow.value=true
|
||||
data.name=name
|
||||
data.code=code
|
||||
data.fenshiURL='https://image.sinajs.cn/newchart/v5/fund/nav/ss/'+code+'.gif'+"?t="+Date.now()
|
||||
}
|
||||
|
||||
function blinkBorder(findId){
|
||||
// 获取要滚动到的元素
|
||||
const element = document.getElementById(findId);
|
||||
if (element) {
|
||||
// 滚动到该元素
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
const pelement = document.getElementById(findId +'_gi');
|
||||
if(pelement){
|
||||
// 添加闪烁效果
|
||||
pelement.classList.add('blink-border');
|
||||
// 3秒后移除闪烁效果
|
||||
setTimeout(() => {
|
||||
pelement.classList.remove('blink-border');
|
||||
}, 1000*5);
|
||||
}else{
|
||||
console.error(`Element with ID ${findId}_gi not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vue-danmaku v-model:danmus="danmus" useSlot style="height:100px; width:100%;z-index: 9;position:absolute; top: 400px; pointer-events: none;" >
|
||||
<template v-slot:dm="{ index, danmu }">
|
||||
<n-gradient-text type="info">
|
||||
<n-icon :component="ChatboxOutline"/>{{ danmu }}
|
||||
</n-gradient-text>
|
||||
</template>
|
||||
</vue-danmaku>
|
||||
<n-flex justify="start" >
|
||||
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
|
||||
<n-gi :id="info.code+'_gi'" v-for="info in followList" style="margin-left: 2px" >
|
||||
<n-card :id="info.code" :title="formatterTitle(info.name)">
|
||||
<template #header-extra>
|
||||
<n-tag size="small" :bordered="false" type="info">{{info.code}}</n-tag>
|
||||
<n-tag size="small" :bordered="false" type="success" @click="unFollow(info.code)"> 取消关注</n-tag>
|
||||
</template>
|
||||
<n-flex>
|
||||
<n-text size="small" :type="info.netEstimatedRate>0?'error':'success'" :bordered="false" v-if="info.netEstimatedUnit">
|
||||
估算净值:{{info.netEstimatedUnit}}
|
||||
{{info.netEstimatedRate}} %
|
||||
({{info.netEstimatedUnitTime}})</n-text>
|
||||
<br>
|
||||
<n-text size="small" :type="info.netEstimatedRate>0?'error':'success'" :bordered="false" v-if="info.netUnitValue">
|
||||
单位净值:{{info.netUnitValue}} ({{info.netUnitValueDate}})</n-text>
|
||||
</n-flex>
|
||||
<n-flex justify="start" style="margin-top: 10px">
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth1>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth1">近一月:{{info.fundBasic.netGrowth1}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth3>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth3">近三月:{{info.fundBasic.netGrowth3}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth6>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth6">近六月:{{info.fundBasic.netGrowth6}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth12>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth12">近一年:{{info.fundBasic.netGrowth12}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth36>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth36">近三年:{{info.fundBasic.netGrowth36}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowth60>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth60">近五年:{{info.fundBasic.netGrowth60}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowthYTD>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowthYTD" >今年来:{{info.fundBasic.netGrowthYTD}}%</n-tag>
|
||||
<n-tag size="small" :type="info.fundBasic.netGrowthAll>0?'error':'success'" :bordered="false" >成立来:{{info.fundBasic.netGrowthAll}}%</n-tag>
|
||||
</n-flex>
|
||||
<template #footer>
|
||||
<n-flex justify="space-between">
|
||||
<n-tag size="small" :bordered="false" type="warning"> {{info.fundBasic.type}}</n-tag>
|
||||
<n-tag size="small" :bordered="false" type="info"> {{info.fundBasic.company}}:{{info.fundBasic.manager}}</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #action>
|
||||
<n-flex justify="end">
|
||||
<n-button size="tiny" type="error" @click="newchart(info.code,info.name)"> 走势 </n-button>
|
||||
<n-button size="tiny" type="warning" @click="search(info.code,info.name)"> 详情 </n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
|
||||
<n-modal v-model:show="modalShow" :title="data.name" style="width: 400px" :preset="'card'">
|
||||
<n-image :src="data.fenshiURL" />
|
||||
</n-modal>
|
||||
|
||||
<div style="position: fixed;bottom: 18px;right:5px;z-index: 10;width: 400px">
|
||||
<n-input-group >
|
||||
<n-auto-complete v-model:value="data.name"
|
||||
:input-props="{
|
||||
autocomplete: 'disabled',
|
||||
}"
|
||||
:options="options"
|
||||
placeholder="基金名称/代码/弹幕"
|
||||
clearable @update-value="getFundList" :on-select="onSelectFund"/>
|
||||
<n-button type="primary" @click="AddFund" >
|
||||
<n-icon :component="Add"/>
|
||||
关注
|
||||
</n-button>
|
||||
<n-button type="info" @click="SendDanmu" v-if="data.enableDanmu" >
|
||||
<n-icon :component="ChatboxOutline"/>
|
||||
发送弹幕
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 添加闪烁效果的CSS类 */
|
||||
.blink-border {
|
||||
animation: blink-border 1s linear infinite;
|
||||
border: 4px solid transparent;
|
||||
}
|
||||
|
||||
@keyframes blink-border {
|
||||
0% {
|
||||
border-color: red;
|
||||
}
|
||||
50% {
|
||||
border-color: transparent;
|
||||
}
|
||||
100% {
|
||||
border-color: red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
324
frontend/src/components/settings.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<script setup>
|
||||
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {ExportConfig, GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App";
|
||||
import {useMessage} from "naive-ui";
|
||||
import {data} from "../../wailsjs/go/models";
|
||||
import {EventsEmit} from "../../wailsjs/runtime";
|
||||
const message = useMessage()
|
||||
|
||||
const formRef = ref(null)
|
||||
const formValue = ref({
|
||||
ID:1,
|
||||
tushareToken:'',
|
||||
dingPush:{
|
||||
enable:false,
|
||||
dingRobot: ''
|
||||
},
|
||||
localPush:{
|
||||
enable:true,
|
||||
},
|
||||
updateBasicInfoOnStart:false,
|
||||
refreshInterval:1,
|
||||
openAI:{
|
||||
enable:false,
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
apiKey: '',
|
||||
model: 'deepseek-chat',
|
||||
temperature: 0.1,
|
||||
maxTokens: 1024,
|
||||
prompt:"",
|
||||
timeout: 5,
|
||||
questionTemplate: "{{stockName}}分析和总结",
|
||||
crawlTimeOut:30,
|
||||
kDays:30,
|
||||
},
|
||||
enableDanmu:false,
|
||||
browserPath: '',
|
||||
enableNews:false,
|
||||
darkTheme:true,
|
||||
})
|
||||
|
||||
onMounted(()=>{
|
||||
GetConfig().then(res=>{
|
||||
formValue.value.ID = res.ID
|
||||
formValue.value.tushareToken = res.tushareToken
|
||||
formValue.value.dingPush = {
|
||||
enable:res.dingPushEnable,
|
||||
dingRobot:res.dingRobot
|
||||
}
|
||||
formValue.value.localPush = {
|
||||
enable:res.localPushEnable,
|
||||
}
|
||||
formValue.value.updateBasicInfoOnStart = res.updateBasicInfoOnStart
|
||||
formValue.value.refreshInterval = res.refreshInterval
|
||||
formValue.value.openAI = {
|
||||
enable:res.openAiEnable,
|
||||
baseUrl: res.openAiBaseUrl,
|
||||
apiKey:res.openAiApiKey,
|
||||
model:res.openAiModelName,
|
||||
temperature:res.openAiTemperature,
|
||||
maxTokens:res.openAiMaxTokens,
|
||||
prompt:res.prompt,
|
||||
timeout:res.openAiApiTimeOut,
|
||||
questionTemplate:res.questionTemplate?res.questionTemplate:'{{stockName}}分析和总结',
|
||||
crawlTimeOut:res.crawlTimeOut,
|
||||
kDays:res.kDays,
|
||||
}
|
||||
formValue.value.enableDanmu = res.enableDanmu
|
||||
formValue.value.browserPath = res.browserPath
|
||||
formValue.value.enableNews = res.enableNews
|
||||
formValue.value.darkTheme = res.darkTheme
|
||||
|
||||
console.log(res)
|
||||
})
|
||||
//message.info("加载完成")
|
||||
})
|
||||
|
||||
|
||||
function saveConfig(){
|
||||
let config= new data.Settings({
|
||||
ID:formValue.value.ID,
|
||||
dingPushEnable:formValue.value.dingPush.enable,
|
||||
dingRobot:formValue.value.dingPush.dingRobot,
|
||||
localPushEnable:formValue.value.localPush.enable,
|
||||
updateBasicInfoOnStart:formValue.value.updateBasicInfoOnStart,
|
||||
refreshInterval:formValue.value.refreshInterval,
|
||||
openAiEnable:formValue.value.openAI.enable,
|
||||
openAiBaseUrl:formValue.value.openAI.baseUrl,
|
||||
openAiApiKey:formValue.value.openAI.apiKey,
|
||||
openAiModelName:formValue.value.openAI.model,
|
||||
openAiMaxTokens:formValue.value.openAI.maxTokens,
|
||||
openAiTemperature:formValue.value.openAI.temperature,
|
||||
tushareToken:formValue.value.tushareToken,
|
||||
prompt:formValue.value.openAI.prompt,
|
||||
openAiApiTimeOut:formValue.value.openAI.timeout,
|
||||
questionTemplate:formValue.value.openAI.questionTemplate,
|
||||
crawlTimeOut:formValue.value.openAI.crawlTimeOut,
|
||||
kDays:formValue.value.openAI.kDays,
|
||||
enableDanmu:formValue.value.enableDanmu,
|
||||
browserPath:formValue.value.browserPath,
|
||||
enableNews:formValue.value.enableNews,
|
||||
darkTheme:formValue.value.darkTheme
|
||||
})
|
||||
|
||||
//console.log("Settings",config)
|
||||
UpdateConfig(config).then(res=>{
|
||||
message.success(res)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function getHeight() {
|
||||
return document.documentElement.clientHeight
|
||||
}
|
||||
function sendTestNotice(){
|
||||
let markdown="### go-stock test\n"+new Date()
|
||||
let msg='{' +
|
||||
' "msgtype": "markdown",' +
|
||||
' "markdown": {' +
|
||||
' "title":"go-stock'+new Date()+'",' +
|
||||
' "text": "'+markdown+'"' +
|
||||
' },' +
|
||||
' "at": {' +
|
||||
' "isAtAll": true' +
|
||||
' }' +
|
||||
' }'
|
||||
|
||||
SendDingDingMessageByType(msg, "test-"+new Date().getTime(),1).then(res=>{
|
||||
message.info(res)
|
||||
})
|
||||
}
|
||||
|
||||
function exportConfig(){
|
||||
ExportConfig().then(res=>{
|
||||
message.info(res)
|
||||
})
|
||||
}
|
||||
|
||||
function importConfig(){
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = (e) => {
|
||||
let file = e.target.files[0];
|
||||
let reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
let config = JSON.parse(e.target.result);
|
||||
console.log(config)
|
||||
formValue.value.ID = config.ID
|
||||
formValue.value.tushareToken = config.tushareToken
|
||||
formValue.value.dingPush = {
|
||||
enable:config.dingPushEnable,
|
||||
dingRobot:config.dingRobot
|
||||
}
|
||||
formValue.value.localPush = {
|
||||
enable:config.localPushEnable,
|
||||
}
|
||||
formValue.value.updateBasicInfoOnStart = config.updateBasicInfoOnStart
|
||||
formValue.value.refreshInterval = config.refreshInterval
|
||||
formValue.value.openAI = {
|
||||
enable:config.openAiEnable,
|
||||
baseUrl: config.openAiBaseUrl,
|
||||
apiKey:config.openAiApiKey,
|
||||
model:config.openAiModelName,
|
||||
temperature:config.openAiTemperature,
|
||||
maxTokens:config.openAiMaxTokens,
|
||||
prompt:config.prompt,
|
||||
timeout:config.openAiApiTimeOut,
|
||||
questionTemplate:config.questionTemplate,
|
||||
crawlTimeOut:config.crawlTimeOut,
|
||||
kDays:config.kDays
|
||||
}
|
||||
formValue.value.enableDanmu = config.enableDanmu
|
||||
formValue.value.browserPath = config.browserPath
|
||||
formValue.value.enableNews = config.enableNews
|
||||
formValue.value.darkTheme = config.darkTheme
|
||||
// formRef.value.resetFields()
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
|
||||
window.onerror = function (event, source, lineno, colno, error) {
|
||||
console.log(event, source, lineno, colno, error)
|
||||
// 将错误信息发送给后端
|
||||
EventsEmit("frontendError", {
|
||||
page: "settings.vue",
|
||||
message: event,
|
||||
source: source,
|
||||
lineno: lineno,
|
||||
colno: colno,
|
||||
error: error ? error.stack : null
|
||||
});
|
||||
//message.error("发生错误:"+event)
|
||||
return true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px">
|
||||
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" >
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left" >
|
||||
<n-gi :span="24">
|
||||
<n-text type="primary" style="font-size: 25px;font-weight: bold">基础设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="10" label="Tushare api token:" path="tushareToken" >
|
||||
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
|
||||
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="数据刷新间隔(重启生效):" path="refreshInterval" >
|
||||
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
|
||||
<template #suffix>
|
||||
秒
|
||||
</template>
|
||||
</n-input-number>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="暗黑主题(重启生效):" path="darkTheme" >
|
||||
<n-switch v-model:value="formValue.darkTheme" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" label="浏览器路径:" path="browserPath" >
|
||||
<n-input type="text" placeholder="浏览器路径" v-model:value="formValue.browserPath" clearable />
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left">
|
||||
<n-gi :span="24">
|
||||
<n-text type="primary" style="font-size: 25px;font-weight: bold">通知设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="6" label="是否启用钉钉推送:" path="dingPush.enable" >
|
||||
<n-switch v-model:value="formValue.dingPush.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" label="是否启用本地推送:" path="localPush.enable" >
|
||||
<n-switch v-model:value="formValue.localPush.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="弹幕功能:" path="enableDanmu" >
|
||||
<n-switch v-model:value="formValue.enableDanmu" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="是否显示滚动快讯(重启生效):" path="enableNews" >
|
||||
<n-switch v-model:value="formValue.enableNews" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
|
||||
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
|
||||
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
|
||||
<n-gi :span="24">
|
||||
<n-text type="primary" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="3" label="是否启用AI诊股:" path="openAI.enable" >
|
||||
<n-switch v-model:value="formValue.openAI.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="9" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl" >
|
||||
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="AI Timeout(秒):" title="AI请求超时时间(秒)" path="openAI.timeout" >
|
||||
<n-input-number min="60" step="1" placeholder="AI请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="Crawler Timeout(秒):" title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut" >
|
||||
<n-input-number min="30" step="1" placeholder="资讯采集超时时间(秒)" v-model:value="formValue.openAI.crawlTimeOut" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey):" path="openAI.apiKey" >
|
||||
<n-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称:" path="openAI.model" >
|
||||
<n-input type="text" placeholder="AI模型名称" v-model:value="formValue.openAI.model" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI temperature:" path="openAI.temperature" >
|
||||
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="openAI maxTokens:" path="openAI.maxTokens" >
|
||||
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天):" path="openAI.maxTokens" >
|
||||
<n-input-number min="30" step="1" max="365" placeholder="日K线数据(天)" title="天数越多消耗tokens越多" v-model:value="formValue.openAI.kDays"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型系统 Prompt:" path="openAI.prompt" >
|
||||
<n-input v-model:value="formValue.openAI.prompt"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入系统prompt"
|
||||
:autosize="{
|
||||
minRows: 5,
|
||||
maxRows: 8
|
||||
}"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型用户 Prompt:" path="openAI.questionTemplate" >
|
||||
<n-input v-model:value="formValue.openAI.questionTemplate"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入用户prompt:例如{{stockName}}[{{stockCode}}]分析和总结"
|
||||
:autosize="{
|
||||
minRows: 5,
|
||||
maxRows: 8
|
||||
}"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
<n-gi :span="24">
|
||||
<n-space justify="center">
|
||||
<n-button type="primary" @click="saveConfig">
|
||||
保存
|
||||
</n-button>
|
||||
<n-button type="info" @click="exportConfig">
|
||||
导出
|
||||
</n-button>
|
||||
<n-button type="error" @click="importConfig">
|
||||
导入
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
</n-form>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,10 +1,11 @@
|
||||
import {createApp} from 'vue'
|
||||
import naive from 'naive-ui'
|
||||
import App from './App.vue'
|
||||
import router from './router/router'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(naive)
|
||||
|
||||
app.mount('#app')
|
||||
20
frontend/src/router/router.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
|
||||
import stockView from '../components/stock.vue'
|
||||
import settingsView from '../components/settings.vue'
|
||||
import about from "../components/about.vue";
|
||||
import fundView from "../components/fund.vue";
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: stockView,name: 'stock' },
|
||||
{ path: '/fund', component: fundView,name: 'fund' },
|
||||
{ path: '/settings', component: settingsView,name: 'settings' },
|
||||
{ path: '/about', component: about,name: 'about' },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,12 +1,12 @@
|
||||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
/*background-color: rgba(27, 38, 54, 1);*/
|
||||
text-align: center;
|
||||
color: white;
|
||||
/*color: white;*/
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
/*color: white;*/
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
|
||||
37
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -1,15 +1,50 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {models} from '../models';
|
||||
import {data} from '../models';
|
||||
|
||||
export function CheckUpdate():Promise<void>;
|
||||
|
||||
export function ExportConfig():Promise<string>;
|
||||
|
||||
export function Follow(arg1:string):Promise<string>;
|
||||
|
||||
export function GetFollowList():Promise<Array<data.FollowedStock>>;
|
||||
export function FollowFund(arg1:string):Promise<string>;
|
||||
|
||||
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
|
||||
|
||||
export function GetConfig():Promise<data.Settings>;
|
||||
|
||||
export function GetFollowList():Promise<any>;
|
||||
|
||||
export function GetFollowedFund():Promise<Array<data.FollowedFund>>;
|
||||
|
||||
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
|
||||
|
||||
export function GetVersionInfo():Promise<models.VersionInfo>;
|
||||
|
||||
export function GetfundList(arg1:string):Promise<Array<data.FundBasic>>;
|
||||
|
||||
export function Greet(arg1:string):Promise<data.StockInfo>;
|
||||
|
||||
export function NewChatStream(arg1:string,arg2:string,arg3:string):Promise<void>;
|
||||
|
||||
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
|
||||
|
||||
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function SendDingDingMessageByType(arg1:string,arg2:string,arg3:number):Promise<string>;
|
||||
|
||||
export function SetAlarmChangePercent(arg1:number,arg2:number,arg3:string):Promise<string>;
|
||||
|
||||
export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>;
|
||||
|
||||
export function SetStockSort(arg1:number,arg2:string):Promise<void>;
|
||||
|
||||
export function ShareAnalysis(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function UnFollow(arg1:string):Promise<string>;
|
||||
|
||||
export function UnFollowFund(arg1:string):Promise<string>;
|
||||
|
||||
export function UpdateConfig(arg1:data.Settings):Promise<string>;
|
||||
|
||||
@@ -2,26 +2,94 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function CheckUpdate() {
|
||||
return window['go']['main']['App']['CheckUpdate']();
|
||||
}
|
||||
|
||||
export function ExportConfig() {
|
||||
return window['go']['main']['App']['ExportConfig']();
|
||||
}
|
||||
|
||||
export function Follow(arg1) {
|
||||
return window['go']['main']['App']['Follow'](arg1);
|
||||
}
|
||||
|
||||
export function FollowFund(arg1) {
|
||||
return window['go']['main']['App']['FollowFund'](arg1);
|
||||
}
|
||||
|
||||
export function GetAIResponseResult(arg1) {
|
||||
return window['go']['main']['App']['GetAIResponseResult'](arg1);
|
||||
}
|
||||
|
||||
export function GetConfig() {
|
||||
return window['go']['main']['App']['GetConfig']();
|
||||
}
|
||||
|
||||
export function GetFollowList() {
|
||||
return window['go']['main']['App']['GetFollowList']();
|
||||
}
|
||||
|
||||
export function GetFollowedFund() {
|
||||
return window['go']['main']['App']['GetFollowedFund']();
|
||||
}
|
||||
|
||||
export function GetStockList(arg1) {
|
||||
return window['go']['main']['App']['GetStockList'](arg1);
|
||||
}
|
||||
|
||||
export function GetVersionInfo() {
|
||||
return window['go']['main']['App']['GetVersionInfo']();
|
||||
}
|
||||
|
||||
export function GetfundList(arg1) {
|
||||
return window['go']['main']['App']['GetfundList'](arg1);
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function NewChatStream(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
|
||||
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
|
||||
}
|
||||
|
||||
export function SendDingDingMessage(arg1, arg2) {
|
||||
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SendDingDingMessageByType(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SendDingDingMessageByType'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SetAlarmChangePercent(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SetAlarmChangePercent'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SetCostPriceAndVolume(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SetStockSort(arg1, arg2) {
|
||||
return window['go']['main']['App']['SetStockSort'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ShareAnalysis(arg1, arg2) {
|
||||
return window['go']['main']['App']['ShareAnalysis'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function UnFollow(arg1) {
|
||||
return window['go']['main']['App']['UnFollow'](arg1);
|
||||
}
|
||||
|
||||
export function UnFollowFund(arg1) {
|
||||
return window['go']['main']['App']['UnFollowFund'](arg1);
|
||||
}
|
||||
|
||||
export function UpdateConfig(arg1) {
|
||||
return window['go']['main']['App']['UpdateConfig'](arg1);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,211 @@
|
||||
export namespace data {
|
||||
|
||||
export class FollowedStock {
|
||||
StockCode: string;
|
||||
Name: string;
|
||||
Volume: number;
|
||||
CostPrice: number;
|
||||
Price: number;
|
||||
PriceChange: number;
|
||||
ChangePercent: number;
|
||||
export class FundBasic {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
Time: any;
|
||||
Sort: number;
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
code: string;
|
||||
name: string;
|
||||
fullName: string;
|
||||
type: string;
|
||||
establishment: string;
|
||||
scale: string;
|
||||
company: string;
|
||||
manager: string;
|
||||
rating: string;
|
||||
trackingTarget: string;
|
||||
netUnitValue?: number;
|
||||
netUnitValueDate: string;
|
||||
netEstimatedUnit?: number;
|
||||
netEstimatedUnitTime: string;
|
||||
netAccumulated?: number;
|
||||
netGrowth1?: number;
|
||||
netGrowth3?: number;
|
||||
netGrowth6?: number;
|
||||
netGrowth12?: number;
|
||||
netGrowth36?: number;
|
||||
netGrowth60?: number;
|
||||
netGrowthYTD?: number;
|
||||
netGrowthAll?: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedStock(source);
|
||||
return new FundBasic(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.StockCode = source["StockCode"];
|
||||
this.Name = source["Name"];
|
||||
this.Volume = source["Volume"];
|
||||
this.CostPrice = source["CostPrice"];
|
||||
this.Price = source["Price"];
|
||||
this.PriceChange = source["PriceChange"];
|
||||
this.ChangePercent = source["ChangePercent"];
|
||||
this.Time = this.convertValues(source["Time"], null);
|
||||
this.Sort = source["Sort"];
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.code = source["code"];
|
||||
this.name = source["name"];
|
||||
this.fullName = source["fullName"];
|
||||
this.type = source["type"];
|
||||
this.establishment = source["establishment"];
|
||||
this.scale = source["scale"];
|
||||
this.company = source["company"];
|
||||
this.manager = source["manager"];
|
||||
this.rating = source["rating"];
|
||||
this.trackingTarget = source["trackingTarget"];
|
||||
this.netUnitValue = source["netUnitValue"];
|
||||
this.netUnitValueDate = source["netUnitValueDate"];
|
||||
this.netEstimatedUnit = source["netEstimatedUnit"];
|
||||
this.netEstimatedUnitTime = source["netEstimatedUnitTime"];
|
||||
this.netAccumulated = source["netAccumulated"];
|
||||
this.netGrowth1 = source["netGrowth1"];
|
||||
this.netGrowth3 = source["netGrowth3"];
|
||||
this.netGrowth6 = source["netGrowth6"];
|
||||
this.netGrowth12 = source["netGrowth12"];
|
||||
this.netGrowth36 = source["netGrowth36"];
|
||||
this.netGrowth60 = source["netGrowth60"];
|
||||
this.netGrowthYTD = source["netGrowthYTD"];
|
||||
this.netGrowthAll = source["netGrowthAll"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class FollowedFund {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
code: string;
|
||||
name: string;
|
||||
netUnitValue?: number;
|
||||
netUnitValueDate: string;
|
||||
netEstimatedUnit?: number;
|
||||
netEstimatedUnitTime: string;
|
||||
netAccumulated?: number;
|
||||
netEstimatedRate?: number;
|
||||
fundBasic: FundBasic;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedFund(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.code = source["code"];
|
||||
this.name = source["name"];
|
||||
this.netUnitValue = source["netUnitValue"];
|
||||
this.netUnitValueDate = source["netUnitValueDate"];
|
||||
this.netEstimatedUnit = source["netEstimatedUnit"];
|
||||
this.netEstimatedUnitTime = source["netEstimatedUnitTime"];
|
||||
this.netAccumulated = source["netAccumulated"];
|
||||
this.netEstimatedRate = source["netEstimatedRate"];
|
||||
this.fundBasic = this.convertValues(source["fundBasic"], FundBasic);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
export class Settings {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
tushareToken: string;
|
||||
localPushEnable: boolean;
|
||||
dingPushEnable: boolean;
|
||||
dingRobot: string;
|
||||
updateBasicInfoOnStart: boolean;
|
||||
refreshInterval: number;
|
||||
openAiEnable: boolean;
|
||||
openAiBaseUrl: string;
|
||||
openAiApiKey: string;
|
||||
openAiModelName: string;
|
||||
openAiMaxTokens: number;
|
||||
openAiTemperature: number;
|
||||
openAiApiTimeOut: number;
|
||||
prompt: string;
|
||||
checkUpdate: boolean;
|
||||
questionTemplate: string;
|
||||
crawlTimeOut: number;
|
||||
kDays: number;
|
||||
enableDanmu: boolean;
|
||||
browserPath: string;
|
||||
enableNews: boolean;
|
||||
darkTheme: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Settings(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.tushareToken = source["tushareToken"];
|
||||
this.localPushEnable = source["localPushEnable"];
|
||||
this.dingPushEnable = source["dingPushEnable"];
|
||||
this.dingRobot = source["dingRobot"];
|
||||
this.updateBasicInfoOnStart = source["updateBasicInfoOnStart"];
|
||||
this.refreshInterval = source["refreshInterval"];
|
||||
this.openAiEnable = source["openAiEnable"];
|
||||
this.openAiBaseUrl = source["openAiBaseUrl"];
|
||||
this.openAiApiKey = source["openAiApiKey"];
|
||||
this.openAiModelName = source["openAiModelName"];
|
||||
this.openAiMaxTokens = source["openAiMaxTokens"];
|
||||
this.openAiTemperature = source["openAiTemperature"];
|
||||
this.openAiApiTimeOut = source["openAiApiTimeOut"];
|
||||
this.prompt = source["prompt"];
|
||||
this.checkUpdate = source["checkUpdate"];
|
||||
this.questionTemplate = source["questionTemplate"];
|
||||
this.crawlTimeOut = source["crawlTimeOut"];
|
||||
this.kDays = source["kDays"];
|
||||
this.enableDanmu = source["enableDanmu"];
|
||||
this.browserPath = source["browserPath"];
|
||||
this.enableNews = source["enableNews"];
|
||||
this.darkTheme = source["darkTheme"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@@ -132,13 +311,14 @@ export namespace data {
|
||||
"时间": string;
|
||||
"股票代码": string;
|
||||
"股票名称": string;
|
||||
"上次当前价格": number;
|
||||
"当前价格": string;
|
||||
"成交的股票数": string;
|
||||
"成交金额": string;
|
||||
"今日开盘价": string;
|
||||
"昨日收盘价": string;
|
||||
"今日最低价": string;
|
||||
"今日最高价": string;
|
||||
"今日最低价": string;
|
||||
"竞买价": string;
|
||||
"竞卖价": string;
|
||||
"买一报价": string;
|
||||
@@ -161,6 +341,21 @@ export namespace data {
|
||||
"卖四申报": string;
|
||||
"卖五报价": string;
|
||||
"卖五申报": string;
|
||||
"市场": string;
|
||||
"盘前盘后": string;
|
||||
"盘前盘后涨跌幅": string;
|
||||
changePercent: number;
|
||||
changePrice: number;
|
||||
highRate: number;
|
||||
lowRate: number;
|
||||
costPrice: number;
|
||||
costVolume: number;
|
||||
profit: number;
|
||||
profitAmount: number;
|
||||
profitAmountToday: number;
|
||||
sort: number;
|
||||
alarmChangePercent: number;
|
||||
alarmPrice: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new StockInfo(source);
|
||||
@@ -176,13 +371,14 @@ export namespace data {
|
||||
this["时间"] = source["时间"];
|
||||
this["股票代码"] = source["股票代码"];
|
||||
this["股票名称"] = source["股票名称"];
|
||||
this["上次当前价格"] = source["上次当前价格"];
|
||||
this["当前价格"] = source["当前价格"];
|
||||
this["成交的股票数"] = source["成交的股票数"];
|
||||
this["成交金额"] = source["成交金额"];
|
||||
this["今日开盘价"] = source["今日开盘价"];
|
||||
this["昨日收盘价"] = source["昨日收盘价"];
|
||||
this["今日最低价"] = source["今日最低价"];
|
||||
this["今日最高价"] = source["今日最高价"];
|
||||
this["今日最低价"] = source["今日最低价"];
|
||||
this["竞买价"] = source["竞买价"];
|
||||
this["竞卖价"] = source["竞卖价"];
|
||||
this["买一报价"] = source["买一报价"];
|
||||
@@ -205,6 +401,132 @@ export namespace data {
|
||||
this["卖四申报"] = source["卖四申报"];
|
||||
this["卖五报价"] = source["卖五报价"];
|
||||
this["卖五申报"] = source["卖五申报"];
|
||||
this["市场"] = source["市场"];
|
||||
this["盘前盘后"] = source["盘前盘后"];
|
||||
this["盘前盘后涨跌幅"] = source["盘前盘后涨跌幅"];
|
||||
this.changePercent = source["changePercent"];
|
||||
this.changePrice = source["changePrice"];
|
||||
this.highRate = source["highRate"];
|
||||
this.lowRate = source["lowRate"];
|
||||
this.costPrice = source["costPrice"];
|
||||
this.costVolume = source["costVolume"];
|
||||
this.profit = source["profit"];
|
||||
this.profitAmount = source["profitAmount"];
|
||||
this.profitAmountToday = source["profitAmountToday"];
|
||||
this.sort = source["sort"];
|
||||
this.alarmChangePercent = source["alarmChangePercent"];
|
||||
this.alarmPrice = source["alarmPrice"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace models {
|
||||
|
||||
export class AIResponseResult {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
chatId: string;
|
||||
modelName: string;
|
||||
stockCode: string;
|
||||
stockName: string;
|
||||
question: string;
|
||||
content: string;
|
||||
IsDel: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new AIResponseResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.chatId = source["chatId"];
|
||||
this.modelName = source["modelName"];
|
||||
this.stockCode = source["stockCode"];
|
||||
this.stockName = source["stockName"];
|
||||
this.question = source["question"];
|
||||
this.content = source["content"];
|
||||
this.IsDel = source["IsDel"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class VersionInfo {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
version: string;
|
||||
content: string;
|
||||
icon: string;
|
||||
alipay: string;
|
||||
wxpay: string;
|
||||
buildTimeStamp: number;
|
||||
IsDel: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new VersionInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.version = source["version"];
|
||||
this.content = source["content"];
|
||||
this.icon = source["icon"];
|
||||
this.alipay = source["alipay"];
|
||||
this.wxpay = source["wxpay"];
|
||||
this.buildTimeStamp = source["buildTimeStamp"];
|
||||
this.IsDel = source["IsDel"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
|
||||
2
frontend/wailsjs/runtime/runtime.d.ts
vendored
@@ -134,7 +134,7 @@ export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): Promise<Size>;
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
|
||||
66
go.mod
@@ -1,54 +1,82 @@
|
||||
module go-stock
|
||||
|
||||
go 1.21
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/chromedp/chromedp v0.11.2
|
||||
github.com/coocood/freecache v1.2.4
|
||||
github.com/duke-git/lancet/v2 v2.3.4
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
github.com/wailsapp/wails/v2 v2.9.2
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/text v0.16.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/text v0.22.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/gorm v1.25.12
|
||||
gorm.io/plugin/dbresolver v1.5.3
|
||||
gorm.io/plugin/soft_delete v1.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/labstack/echo/v4 v4.10.2 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
|
||||
github.com/leaanthony/gosod v1.0.3 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/samber/lo v1.38.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.6 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.16 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
|
||||
203
go.sum
@@ -1,5 +1,19 @@
|
||||
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb h1:noKVm2SsG4v0Yd0lHNtFYc9EUxIVvrr4kJ6hM8wvIYU=
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM=
|
||||
github.com/chromedp/chromedp v0.11.2 h1:ZRHTh7DjbNTlfIv3NFTbB7eVeu5XCNkgrpcGSpn2oX0=
|
||||
github.com/chromedp/chromedp v0.11.2/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
|
||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
|
||||
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -7,54 +21,100 @@ github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvY
|
||||
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
|
||||
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
|
||||
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
|
||||
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
|
||||
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
|
||||
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -63,27 +123,27 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
|
||||
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.16 h1:wffnvnkkLvhRex/aOrA3R7FP7rkvOqL/bir1br7BekU=
|
||||
github.com/wailsapp/go-webview2 v1.0.16/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
|
||||
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
|
||||
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
|
||||
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
|
||||
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
@@ -93,71 +153,108 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
|
||||
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
|
||||
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
|
||||
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
|
||||
gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU=
|
||||
gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
|
||||
228
main.go
@@ -3,16 +3,24 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/logger"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu/keys"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/mac"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"log"
|
||||
log "go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
@@ -21,50 +29,142 @@ var assets embed.FS
|
||||
//go:embed build/appicon.png
|
||||
var icon []byte
|
||||
|
||||
//go:embed build/app.ico
|
||||
var icon2 []byte
|
||||
|
||||
//go:embed build/screenshot/alipay.jpg
|
||||
var alipay []byte
|
||||
|
||||
//go:embed build/screenshot/wxpay.jpg
|
||||
var wxpay []byte
|
||||
|
||||
//go:embed build/stock_basic.json
|
||||
var stocksBin []byte
|
||||
|
||||
//go:embed build/stock_base_info_hk.json
|
||||
var stocksBinHK []byte
|
||||
|
||||
//go:embed build/stock_base_info_us.json
|
||||
var stocksBinUS []byte
|
||||
|
||||
//go:generate cp -R ./data ./build/bin
|
||||
|
||||
var Version string
|
||||
var VersionCommit string
|
||||
|
||||
func main() {
|
||||
checkDir("data")
|
||||
db.Init("")
|
||||
db.Dao.AutoMigrate(&data.StockInfo{})
|
||||
db.Dao.AutoMigrate(&data.StockBasic{})
|
||||
db.Dao.AutoMigrate(&data.FollowedStock{})
|
||||
db.Dao.AutoMigrate(&data.IndexBasic{})
|
||||
db.Dao.AutoMigrate(&data.Settings{})
|
||||
db.Dao.AutoMigrate(&models.AIResponseResult{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoHK{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoUS{})
|
||||
db.Dao.AutoMigrate(&data.FollowedFund{})
|
||||
db.Dao.AutoMigrate(&data.FundBasic{})
|
||||
|
||||
if stocksBin != nil && len(stocksBin) > 0 {
|
||||
initStockData()
|
||||
go initStockData()
|
||||
}
|
||||
log.SugaredLogger.Infof("init stocksBinHK %d", len(stocksBinHK))
|
||||
|
||||
data.NewStockDataApi().GetStockBaseInfo()
|
||||
if stocksBinHK != nil && len(stocksBinHK) > 0 {
|
||||
go initStockDataHK()
|
||||
}
|
||||
log.SugaredLogger.Infof("init stocksBinUS %d", len(stocksBinUS))
|
||||
|
||||
if stocksBinUS != nil && len(stocksBinUS) > 0 {
|
||||
go initStockDataUS()
|
||||
}
|
||||
updateBasicInfo()
|
||||
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
AppMenu := menu.NewMenu()
|
||||
FileMenu := AppMenu.AddSubmenu("设置")
|
||||
FileMenu.AddText("显示搜索框", keys.CmdOrCtrl("s"), func(callbackData *menu.CallbackData) {
|
||||
runtime.EventsEmit(app.ctx, "showSearch", 1)
|
||||
})
|
||||
FileMenu.AddText("隐藏搜索框", keys.CmdOrCtrl("d"), func(callbackData *menu.CallbackData) {
|
||||
runtime.EventsEmit(app.ctx, "showSearch", 0)
|
||||
})
|
||||
FileMenu.AddText("刷新数据", keys.CmdOrCtrl("r"), func(callbackData *menu.CallbackData) {
|
||||
//runtime.EventsEmit(app.ctx, "refresh", "setting-"+time.Now().Format("2006-01-02 15:04:05"))
|
||||
runtime.EventsEmit(app.ctx, "refreshFollowList", "refresh-"+time.Now().Format("2006-01-02 15:04:05"))
|
||||
})
|
||||
FileMenu.AddSeparator()
|
||||
FileMenu.AddText("窗口全屏", keys.CmdOrCtrl("f"), func(callback *menu.CallbackData) {
|
||||
runtime.WindowFullscreen(app.ctx)
|
||||
})
|
||||
FileMenu.AddText("窗口还原", keys.Key("Esc"), func(callback *menu.CallbackData) {
|
||||
runtime.WindowUnfullscreen(app.ctx)
|
||||
})
|
||||
|
||||
if goruntime.GOOS == "windows" {
|
||||
FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("h"), func(_ *menu.CallbackData) {
|
||||
runtime.WindowHide(app.ctx)
|
||||
})
|
||||
|
||||
FileMenu.AddText("显示", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) {
|
||||
runtime.WindowShow(app.ctx)
|
||||
})
|
||||
}
|
||||
|
||||
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
|
||||
// runtime.Quit(app.ctx)
|
||||
//})
|
||||
log.SugaredLogger.Info("version: " + Version)
|
||||
log.SugaredLogger.Info("commit: " + VersionCommit)
|
||||
// Create application with options
|
||||
//var width, height int
|
||||
//var err error
|
||||
//
|
||||
//width, height, err = getScreenResolution()
|
||||
//if err != nil {
|
||||
// log.SugaredLogger.Error("get screen resolution error")
|
||||
// width = 1456
|
||||
// height = 768
|
||||
//}
|
||||
|
||||
darkTheme := data.NewSettingsApi(&data.Settings{}).GetConfig().DarkTheme
|
||||
backgroundColour := &options.RGBA{R: 255, G: 255, B: 255, A: 1}
|
||||
if darkTheme {
|
||||
backgroundColour = &options.RGBA{R: 27, G: 38, B: 54, A: 1}
|
||||
}
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "go-stock",
|
||||
Width: 1366,
|
||||
Height: 860,
|
||||
MinWidth: 1024,
|
||||
MinHeight: 768,
|
||||
MaxWidth: 1280,
|
||||
MaxHeight: 960,
|
||||
DisableResize: false,
|
||||
Fullscreen: false,
|
||||
Frameless: false,
|
||||
StartHidden: false,
|
||||
HideWindowOnClose: false,
|
||||
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
Assets: assets,
|
||||
Menu: nil,
|
||||
Logger: nil,
|
||||
LogLevel: logger.DEBUG,
|
||||
OnStartup: app.startup,
|
||||
OnDomReady: app.domReady,
|
||||
OnBeforeClose: app.beforeClose,
|
||||
OnShutdown: app.shutdown,
|
||||
WindowStartState: options.Normal,
|
||||
Title: "go-stock",
|
||||
//Width: width * 4 / 5,
|
||||
//Height: height * 4 / 5,
|
||||
MinWidth: 1456,
|
||||
MinHeight: 820,
|
||||
//MaxWidth: width,
|
||||
//MaxHeight: height,
|
||||
DisableResize: false,
|
||||
Fullscreen: false,
|
||||
Frameless: true,
|
||||
StartHidden: false,
|
||||
HideWindowOnClose: false,
|
||||
EnableDefaultContextMenu: true,
|
||||
BackgroundColour: backgroundColour,
|
||||
Assets: assets,
|
||||
Menu: AppMenu,
|
||||
Logger: nil,
|
||||
LogLevel: logger.DEBUG,
|
||||
LogLevelProduction: logger.ERROR,
|
||||
OnStartup: app.startup,
|
||||
OnDomReady: app.domReady,
|
||||
OnBeforeClose: app.beforeClose,
|
||||
OnShutdown: app.shutdown,
|
||||
WindowStartState: options.Normal,
|
||||
SingleInstanceLock: &options.SingleInstanceLock{
|
||||
UniqueId: "go-stock",
|
||||
OnSecondInstanceLaunch: OnSecondInstanceLaunch,
|
||||
},
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
@@ -98,21 +198,64 @@ func main() {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.SugaredLogger.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func initStockDataUS() {
|
||||
var v []models.StockInfoUS
|
||||
err := json.Unmarshal(stocksBinUS, &v)
|
||||
if err != nil {
|
||||
log.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
log.SugaredLogger.Infof("init stock data us %d", len(v))
|
||||
for _, item := range v {
|
||||
var count int64
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", item.Code).Count(&count)
|
||||
if count > 0 {
|
||||
log.SugaredLogger.Infof("stock data us %s exist", item.Code)
|
||||
continue
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Create(&item)
|
||||
}
|
||||
}
|
||||
|
||||
func initStockDataHK() {
|
||||
var v []models.StockInfoHK
|
||||
err := json.Unmarshal(stocksBinHK, &v)
|
||||
if err != nil {
|
||||
log.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
log.SugaredLogger.Infof("init stock data hk %d", len(v))
|
||||
for _, item := range v {
|
||||
var count int64
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", item.Code).Count(&count)
|
||||
if count > 0 {
|
||||
log.SugaredLogger.Infof("stock data hk %s exist", item.Code)
|
||||
continue
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(&item)
|
||||
}
|
||||
}
|
||||
|
||||
func updateBasicInfo() {
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
if config.UpdateBasicInfoOnStart {
|
||||
//更新基本信息
|
||||
go data.NewStockDataApi().GetStockBaseInfo()
|
||||
go data.NewStockDataApi().GetIndexBasic()
|
||||
}
|
||||
}
|
||||
|
||||
func initStockData() {
|
||||
var count int64
|
||||
db.Dao.Model(&data.StockBasic{}).Count(&count)
|
||||
if count > 0 {
|
||||
return
|
||||
}
|
||||
logger.NewDefaultLogger().Info("init stock data")
|
||||
log.SugaredLogger.Info("init stock data")
|
||||
res := &data.TushareStockBasicResponse{}
|
||||
err := json.Unmarshal(stocksBin, res)
|
||||
if err != nil {
|
||||
logger.NewDefaultLogger().Error(err.Error())
|
||||
log.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
for _, item := range res.Data.Items {
|
||||
@@ -134,7 +277,14 @@ func initStockData() {
|
||||
stock.Cnspell = convertor.ToString(item[14])
|
||||
stock.Fullname = convertor.ToString(item[20])
|
||||
stock.Ename = convertor.ToString(item[21])
|
||||
db.Dao.Model(&data.StockBasic{}).FirstOrCreate(stock, &data.StockBasic{TsCode: stock.TsCode}).Updates(stock)
|
||||
|
||||
var count int64
|
||||
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Count(&count)
|
||||
if count > 0 {
|
||||
continue
|
||||
} else {
|
||||
db.Dao.Create(stock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +292,14 @@ func checkDir(dir string) {
|
||||
_, err := os.Stat(dir)
|
||||
if os.IsNotExist(err) {
|
||||
os.Mkdir(dir, os.ModePerm)
|
||||
logger.NewDefaultLogger().Info("create dir: " + dir)
|
||||
log.SugaredLogger.Info("create dir: " + dir)
|
||||
}
|
||||
}
|
||||
|
||||
// PanicHandler 捕获 panic 的包装函数
|
||||
func PanicHandler() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Printf("Recovered from panic: %v\n", r)
|
||||
debug.PrintStack()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"request_id":"359a096c-0a5a-4b07-818f-3241738d5e73","code":40203,"data":null,"msg":"抱歉,您每小时最多访问该接口1次,权限的具体详情访问:https://tushare.pro/document/1?doc_id=108。"}
|
||||
@@ -7,13 +7,13 @@
|
||||
"frontend:dev:serverUrl": "http://localhost:5173",
|
||||
"author": {
|
||||
"name": "spark",
|
||||
"email": "wzl@huazx.cn"
|
||||
"email": "sparkmemory@163.com"
|
||||
},
|
||||
"info": {
|
||||
"companyName": "sparkmemory",
|
||||
"productName": "go-stock",
|
||||
"productVersion": "1.0.0",
|
||||
"copyright": "Copyright#sparkmemory@163.com",
|
||||
"comments": "股票行情实时获取"
|
||||
"comments": "股票行情实时获取,AI赋能分析股票,支持DeepSeek,OpenAI, Ollama,LMStudio,AnythingLLM,硅基流动,火山方舟,阿里云百炼等平台或模型。软件发行版见GitHub:https://github.com/ArvinLovegood/go-stock"
|
||||
}
|
||||
}
|
||||
|
||||