Compare commits

..

No commits in common. "master" and "v2025.1.31.5" have entirely different histories.

97 changed files with 1392 additions and 269228 deletions

View File

@ -1,54 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
# Bug 报告
## 基本信息
### Bug 描述
<请用简洁明了的语言概括 Bug 的核心问题例如登录页面输入错误密码后无提示信息>
### 软件版本信息
<说明你所使用的软件版本在关于界面中可以找到>
### 运行操作系统和环境
- **操作系统**<例如 Windows 10macOS 12.6Ubuntu 22.04 >
- **浏览器(如果是网页应用)**< Chrome 108Firefox 107 同时说明浏览器的版本和是否使用了特殊的插件>
- **其他相关环境信息**<例如运行项目的服务器配置数据库版本等>
## Bug 描述
### 预期行为
<详细描述你认为在正常情况下系统应该呈现的行为例如当用户在登录页面输入错误密码时页面应弹出提示框显示密码错误请重新输入>
### 实际行为
<准确描述实际发生的情况可以包括错误信息页面显示异常功能无法正常使用等具体表现例如当输入错误密码后页面没有任何提示也没有重新聚焦到密码输入框登录按钮依然可点击>
### 复现步骤
<提供详细的步骤让开发者能够按照这些步骤重现 Bug步骤要尽量清晰具体例如
1. 打开项目的登录页面URL[具体登录页面 URL])。
2. 在用户名输入框输入已注册的用户名。
3. 在密码输入框输入错误的密码。
4. 点击登录按钮。>
### 频率
<说明 Bug 出现的频率例如每次都会出现偶尔出现 10% 的概率>
## 相关信息
### 错误日志
<如果有错误日志或控制台输出信息请提供完整的内容可以使用代码块来展示例如>
### 截图或视频
<如果 Bug 涉及页面显示问题或操作流程异常附上相关的截图或录屏视频会非常有帮助可以直接上传截图文件或者提供视频的链接>
### 可能的原因分析(可选)
<如果你对 Bug 产生的原因有一些初步的猜测或分析可以在这里简要说明这有助于开发者更快地定位问题但不是必需的>
## 补充说明
<如果有其他与 Bug 相关但不属于上述分类的信息可以在这里进行补充例如之前是否进行过特定的配置更改是否与其他功能存在关联等>

View File

@ -1,10 +0,0 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View File

@ -1,20 +0,0 @@
---
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.

View File

@ -1,48 +0,0 @@
# 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`,请确保在环境中安装该库。
## 其他补充说明
- 可以在这里提供任何其他需要说明的信息,例如设计文档的链接、相关讨论的记录等。

View File

@ -4,7 +4,7 @@ on:
push: push:
tags: tags:
# Match any new tag # Match any new tag
- '*-release' - '*'
env: env:
# Necessary for most environments as build failure can occur due to OOM issues # Necessary for most environments as build failure can occur due to OOM issues
@ -31,20 +31,11 @@ jobs:
with: with:
submodules: recursive submodules: recursive
- name: Get commit message - name: Build wails
id: get_commit_message uses: ArvinLovegood/wails-build-action@v2.3
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 id: build
with: with:
build-name: ${{ matrix.build.name }} build-name: ${{ matrix.build.name }}
build-platform: ${{ matrix.build.platform }} build-platform: ${{ matrix.build.platform }}
package: true package: true
go-version: '1.24' go-version: '1.23'
build-tags: ${{ github.ref_name }}
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
node-version: '20.x'

1
.gitignore vendored
View File

@ -110,4 +110,3 @@ dist
/build/*.exe /build/*.exe
/build/bin/* /build/bin/*
frontend/package.json.md5 frontend/package.json.md5
/build/us.json

View File

@ -1,217 +0,0 @@
# 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

View File

@ -1,79 +0,0 @@
# 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 中提问。

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright [2025] [sparkmemory@163.com] Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

162
README.md
View File

@ -1,163 +1,53 @@
# go-stock : 基于大语言模型的AI赋能股票分析工具 ![Wails and NaiveUI](./build/appicon.png)
## ![go-stock](./build/appicon.png) ## 自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具
![GitHub Release](https://img.shields.io/github/v/release/ArvinLovegood/go-stock?link=https%3A%2F%2Fgithub.com%2FArvinLovegood%2Fgo-stock%2Freleases&link=https%3A%2F%2Fgithub.com%2FArvinLovegood%2Fgo-stock%2Freleases)
[![GitHub Repo stars](https://img.shields.io/github/stars/ArvinLovegood/go-stock?link=https%3A%2F%2Fgithub.com%2FArvinLovegood%2Fgo-stock)](https://github.com/ArvinLovegood/go-stock)
[![star](https://gitee.com/arvinlovegood_admin/go-stock/badge/star.svg?theme=dark)](https://gitee.com/arvinlovegood_admin/go-stock)
[//]: # ([![star]&#40;https://gitcode.com/ArvinLovegood/go-stock/star/badge.svg&#41;]&#40;https://gitcode.com/ArvinLovegood/go-stock&#41;) ## BIG NEWS !!! 重大更新!!!
- 2025.01.17 新增AI大模型分析股票功能
![img_5.png](build/screenshot/img_10.png)
### 🌟公众号 ## 简介
![扫码_搜索联合传播样式-白色版.png](build/screenshot/%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.png) - 本项目基于Wails和NaiveUI纯属无聊仅供娱乐不喜勿喷。
### 📈 交流群
- QQ交流群2[点击链接加入群聊【go-stock交流群2】892666282](https://qm.qq.com/q/5mYiy6Yxh0)
- 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)
### ✨ 简介
- 本项目基于Wails和NaiveUI开发结合AI大模型构建的股票分析工具。
- 目前已支持A股港股美股未来计划加入基金ETF等支持。
- 支持市场整体/个股情绪分析K线技术指标分析等功能。
- 本项目仅供娱乐不喜勿喷AI分析股票结果仅供学习研究投资有风险请谨慎使用。
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。 - 开发环境主要基于Windows10+,其他平台未测试或功能受限。
### 📦 立即体验
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
## Snapshot
### 💬 支持大模型/平台
| 模型 | 状态 | 备注 |
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [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,deepseek-chat |
| [大模型聚合平台](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) ,[优云智算](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock) |
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
- 优云智算by UCloud万卡规模4090免费用10小时新人注册另增50万tokens海量热门源项目镜像一键部署[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
- 经测试目前硅基流动(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大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意Tushare只需要120积分即可注册完成个人资料补充即可得120积分)[注册链接](https://tushare.pro/register?reg=701944)
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
- 欢迎大家提出宝贵的建议欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
## 🧩 重大功能开发计划
| 功能说明 | 状态 | 备注 |
|-----------------|----|----------------------------------------------------------------------------------------------------------|
| 股票分析知识库 | 🚧 | 未来计划 |
| Ai智能选股 | 🚧 | Ai智能选股功能开发中(下半年重点开发计划) |
| 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.06.30 添加指标选股功能
### 2025.06.27 添加财经日历和重大事件时间轴功能
### 2025.06.25 添加热门股票、事件和话题功能
### 2025.06.18 更新内置股票基础数据,软件内实时市场资讯信息提醒,添加行业研究功能
### 2025.06.15 添加公司公告信息搜索/查看功能
### 2025.06.15 添加个股研报到弹出菜单
### 2025.06.13 添加个股研报功能
### 2025.06.12 添加龙虎榜功能,新增行业排名分类
### 2025.05.30 优化股票分时图显示
### 2025.05.20 修复财联社电报获取问题
### 2025.05.16 优化资金趋势图表组件
### 2025.05.15 重构应用加载和数据初始化逻辑,添加股票资金趋势功能,资金趋势图表增加主力当日净流入数据并优化展示效果
### 2025.05.14 添加个股资金流向功能排行榜增加股票行情K线图弹窗
### 2025.05.13 添加行业排名功能
### 2025.05.09 添加A股盘口数据解析和展示功能
### 2025.05.07 优化分时图的展示
### 2025.04.29 补全港股/美股基础数据,优化港股股价延迟问题,优化初始化逻辑
### 2025.04.25 市场资讯支持AI分析和总结让AI帮你读市场
### 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定
### 2025.04.22 优化K线图展示支持拉伸放大看得更舒服啦
### 2025.04.21 港股美股K线数据获取优化
### 2025.04.01 优化部分设置选项,避免重启软件
### 2025.03.31 优化数据爬取
### 2025.03.30 AI自动定时分析功能
### 2025.03.29 多提示词模板管理AI分析时支持选择不同提示词模板
### 2025.03.28 AI分析结果保存为markdown文件时支持保存位置目录选择
### 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.04.25 市场资讯支持AI分析和总结让AI帮你读市场
![img.png](img.png)
- 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定
![img.png](build/screenshot/img13.png)
![img_13.png](build/screenshot/img_13.png)
- ![img_14.png](build/screenshot/img_14.png)
- 2025.01.17 新增AI大模型分析股票功能
![img_5.png](build/screenshot/img.png)
## 📸 功能截图
![img_1.png](build/screenshot/img_6.png) ![img_1.png](build/screenshot/img_6.png)
### 设置 ### 设置
![img_12.png](build/screenshot/img_4.png) ![img.png](build/screenshot/img_11.png)
### 成本设置 ### 成本设置
![img.png](build/screenshot/img_7.png) ![img.png](build/screenshot/img_7.png)
### 日K ### 日K
![img_12.png](build/screenshot/img_12.png) ![img_2.png](build/screenshot/img_8.png)
### 分时 ### 分时
![img_3.png](build/screenshot/img_9.png) ![img_3.png](build/screenshot/img_9.png)
### 钉钉报警通知 ### 钉钉报警通知
![img_4.png](build/screenshot/img_5.png) ![img_4.png](build/screenshot/img_5.png)
### AI分析股票 ### AI分析股票
![img_5.png](build/screenshot/img.png) ![img_5.png](build/screenshot/img_10.png)
### 版本信息提示
![img_11.png](build/screenshot/img_11.png)
## 💕 感谢以下项目 ## About
- [NaiveUI](https://www.naiveui.com/)
- [Wails](https://wails.io/)
- [Vue](https://vuejs.org/)
- [Vite](https://vitejs.dev/)
- [Tushare](https://tushare.pro/register?reg=701944)
## 😘 赞助我 ### A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
### 都划到这了,如果我的项目对您有帮助,请赞助我吧!😊😊😊 A股数据可视化工具基于Wails和NaiveUI。
| 支付宝 | 微信 |
|-----|-----|
| ![alipay.jpg](build/screenshot/alipay.jpg) | ![wxpay.jpg](build/screenshot/wxpay.jpg) |
## Prerequisites
INSTALL [GO](https://golang.org) AND [Wails](https://wails.io/)
## ⭐ Star History ## Running the Application in Developer Mode
[![Star History Chart](https://api.star-history.com/svg?repos=ArvinLovegood/go-stock&type=Date)](https://star-history.com/#ArvinLovegood/go-stock&Date) The easiest way is to use the Wails CLI: `wails dev`
## 🤖 状态
![Alt](https://repobeats.axiom.co/api/embed/40b07d415a42c2264a18c4fe1b6f182ff1470687.svg "Repobeats analytics image")
## 🐳 关于技术支持申明 This should hot refresh when making changes the Frontend and rebuild when making changes in the Go.
- 本软件基于开源技术构建使用Wails、NaiveUI、Vue、AI大模型等开源项目。 技术上如有问题,可以先向对应的开源社区请求帮助。
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系微信(备注 技术支持)ArvinLovegood
<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood"> ## Building the Application for Production
| 技术支持方式 | 赞助(元) |
|:--------------------------------|:-----:|
| 加 QQ506808970微信ArvinLovegood | 100/次 |
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |
You can build you Application with: `wails build`
## License ## License
[Apache License 2.0](LICENSE) [Apache License 2.0](LICENSE)
## Credits
[NaiveUI](https://www.naiveui.com/)
[Wails](https://wails.io/)
[Vue](https://vuejs.org/)
[Vite](https://vitejs.dev/)

View File

@ -1,56 +0,0 @@
# 安全策略
## 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] 联系我们。

802
app.go
View File

@ -4,336 +4,83 @@ package main
import ( import (
"context" "context"
"encoding/base64"
"encoding/json"
"fmt" "fmt"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"os"
"strings"
"time"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache" "github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor" "github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil" "github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/slice" "github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil" "github.com/getlantern/systray"
"github.com/energye/systray"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/go-toast/toast"
"github.com/robfig/cron/v3"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/sys/windows/registry" "go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"strings"
"time"
) )
// App struct // App struct
type App struct { type App struct {
ctx context.Context ctx context.Context
cache *freecache.Cache cache *freecache.Cache
cron *cron.Cron
cronEntrys map[string]cron.EntryID
} }
// NewApp creates a new App application struct // NewApp creates a new App application struct
func NewApp() *App { func NewApp() *App {
cacheSize := 512 * 1024 cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize) cache := freecache.NewCache(cacheSize)
c := cron.New(cron.WithSeconds())
c.Start()
return &App{ return &App{
cache: cache, cache: cache,
cron: c,
cronEntrys: make(map[string]cron.EntryID),
} }
} }
// startup is called at application startup // startup is called at application startup
func (a *App) startup(ctx context.Context) { 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 // Perform your setup here
a.ctx = ctx a.ctx = ctx
// 创建系统托盘 // 创建系统托盘
//systray.RunWithExternalLoop(func() { systray.Run(func() {
// onReady(a) go onReady(a)
//}, func() {
// onExit(a)
//})
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData)
config := &data.Settings{}
setMap := optionalData[0].(map[string]interface{})
// 将 map 转换为 JSON 字节切片
jsonData, err := json.Marshal(setMap)
if err != nil {
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
return
}
// 将 JSON 字节切片解析到结构体中
err = json.Unmarshal(jsonData, config)
if err != nil {
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
if config.DarkTheme {
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
runtime.WindowSetDarkTheme(ctx)
} else {
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
runtime.WindowSetLightTheme(ctx)
}
runtime.WindowReloadApp(ctx)
})
go systray.Run(func() {
onReady(a)
}, func() { }, func() {
onExit(a) go onExit(a)
}) })
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
}
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 // 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()
if stocksBin != nil && len(stocksBin) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查A股基础信息...")
go initStockData(a.ctx)
}
if stocksBinHK != nil && len(stocksBinHK) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查港股基础信息...")
go initStockDataHK(a.ctx)
}
if stocksBinUS != nil && len(stocksBinUS) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查美股基础信息...")
go initStockDataUS(a.ctx)
}
updateBasicInfo()
// Add your action here // Add your action here
//定时更新数据 //定时更新数据
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
go func() { go func() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
interval := config.RefreshInterval interval := config.RefreshInterval
if interval <= 0 { if interval <= 0 {
interval = 1 interval = 1
} }
//ticker := time.NewTicker(time.Second * time.Duration(interval)) ticker := time.NewTicker(time.Second * time.Duration(interval))
//defer ticker.Stop() defer ticker.Stop()
//for range ticker.C { for range ticker.C {
// MonitorStockPrices(a) if isTradingTime(time.Now()) {
//}
id, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval), func() {
MonitorStockPrices(a) MonitorStockPrices(a)
})
if err != nil {
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
} else {
a.cronEntrys["MonitorStockPrices"] = id
} }
entryID, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval+10), func() {
news := data.NewMarketNewsApi().GetNewTelegraph(30)
if config.EnablePushNews {
go a.NewsPush(news)
}
go runtime.EventsEmit(a.ctx, "newTelegraph", news)
})
if err != nil {
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
} else {
a.cronEntrys["GetNewTelegraph"] = entryID
}
entryIDSina, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval+10), func() {
news := data.NewMarketNewsApi().GetSinaNews(30)
if config.EnablePushNews {
go a.NewsPush(news)
}
go runtime.EventsEmit(a.ctx, "newSinaNews", news)
})
if err != nil {
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
} else {
a.cronEntrys["newSinaNews"] = entryIDSina
} }
}() }()
//刷新基金净值信息
go func() { go func() {
//ticker := time.NewTicker(time.Second * time.Duration(60)) ticker := time.NewTicker(time.Second * time.Duration(60))
//defer ticker.Stop() defer ticker.Stop()
//for range ticker.C { for range ticker.C {
// MonitorFundPrices(a)
//}
if config.EnableFund {
id, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", 60), func() {
MonitorFundPrices(a)
})
if err != nil {
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
} else {
a.cronEntrys["MonitorFundPrices"] = id
}
}
}()
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)
// }
// }
//
//}()
id, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", 60), func() {
telegraph := refreshTelegraphList() telegraph := refreshTelegraphList()
if telegraph != nil { if telegraph != nil {
go runtime.EventsEmit(a.ctx, "telegraph", telegraph) go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
} }
})
if err != nil {
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
} else {
a.cronEntrys["refreshTelegraphList"] = id
} }
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
}
go MonitorStockPrices(a)
if config.EnableFund {
go MonitorFundPrices(a)
go data.NewFundApi().AllFund()
}
//检查新版本
go func() {
a.CheckUpdate()
a.cron.AddFunc("30 05 8,12,20 * * *", func() {
logger.SugaredLogger.Errorf("Checking for updates...")
a.CheckUpdate()
})
}() }()
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
//检查谷歌浏览器 go MonitorStockPrices(a)
//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)
// }
//}()
followList := data.NewStockDataApi().GetFollowList(0)
for _, follow := range *followList {
if follow.Cron == nil || *follow.Cron == "" {
continue
}
entryID, err := a.cron.AddFunc(*follow.Cron, a.AddCronTask(follow))
if err != nil {
logger.SugaredLogger.Errorf("添加自动分析任务失败:%s cron=%s entryID:%v", follow.Name, *follow.Cron, entryID)
continue
}
a.cronEntrys[follow.StockCode] = entryID
}
logger.SugaredLogger.Infof("domReady-cronEntrys:%+v", a.cronEntrys)
}
func (a *App) NewsPush(news *[]models.Telegraph) {
for _, telegraph := range *news {
//if telegraph.IsRed {
go runtime.EventsEmit(a.ctx, "newsPush", telegraph)
go data.NewAlertWindowsApi("go-stock", telegraph.Source+" "+telegraph.Time, telegraph.Content, string(icon)).SendNotification()
//}
}
}
func (a *App) AddCronTask(follow data.FollowedStock) func() {
return func() {
go runtime.EventsEmit(a.ctx, "warnMsg", "开始自动分析"+follow.Name+"_"+follow.StockCode)
ai := data.NewDeepSeekOpenAi(a.ctx)
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil)
var res strings.Builder
chatId := ""
question := ""
for msg := range msgs {
if msg["extraContent"] != nil {
res.WriteString(msg["extraContent"].(string) + "\n")
}
if msg["content"] != nil {
res.WriteString(msg["content"].(string))
}
if msg["chatId"] != nil {
chatId = msg["chatId"].(string)
}
if msg["question"] != nil {
question = msg["question"].(string)
}
}
data.NewDeepSeekOpenAi(a.ctx).SaveAIResponseResult(follow.StockCode, follow.Name, res.String(), chatId, question)
go runtime.EventsEmit(a.ctx, "warnMsg", "AI分析完成"+follow.Name+"_"+follow.StockCode)
}
} }
func refreshTelegraphList() *[]string { func refreshTelegraphList() *[]string {
@ -391,84 +138,6 @@ func isTradingTime(date time.Time) bool {
return false 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) { func MonitorStockPrices(a *App) {
dest := &[]data.FollowedStock{} dest := &[]data.FollowedStock{}
db.Dao.Model(&data.FollowedStock{}).Find(dest) db.Dao.Model(&data.FollowedStock{}).Find(dest)
@ -484,24 +153,11 @@ func MonitorStockPrices(a *App) {
stockInfos := GetStockInfos(*dest...) stockInfos := GetStockInfos(*dest...)
for _, stockInfo := range *stockInfos { 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 total += stockInfo.ProfitAmountToday
price, _ := convertor.ToFloat(stockInfo.Price) price, _ := convertor.ToFloat(stockInfo.Price)
if stockInfo.PrePrice != price { if stockInfo.PrePrice != price {
//logger.SugaredLogger.Infof("-----------sz------------股票代码: %s, 股票名称: %s, 股票价格: %s,盘前盘后:%s", stockInfo.Code, stockInfo.Name, stockInfo.Price, stockInfo.BA)
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo) go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
} }
} }
if total != 0 { if total != 0 {
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total) title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
@ -513,27 +169,18 @@ func MonitorStockPrices(a *App) {
} }
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo { func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockInfos := make([]data.StockInfo, 0)
stockCodes := make([]string, 0) stockCodes := make([]string, 0)
for _, follow := range follows { 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) stockCodes = append(stockCodes, follow.StockCode)
} }
stockData, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...) 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 { for _, info := range *stockData {
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool { 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 return follow.StockCode == info.Code
}) })
if ok { if ok {
@ -561,7 +208,6 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
stockData.CostVolume = follow.Volume //成本量 stockData.CostVolume = follow.Volume //成本量
stockData.AlarmChangePercent = follow.AlarmChangePercent stockData.AlarmChangePercent = follow.AlarmChangePercent
stockData.AlarmPrice = follow.AlarmPrice stockData.AlarmPrice = follow.AlarmPrice
stockData.Groups = follow.Groups
//当前价格 //当前价格
price, _ := convertor.ToFloat(stockData.Price) price, _ := convertor.ToFloat(stockData.Price)
@ -596,14 +242,14 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
//开盘价 //开盘价
//openPrice, _ := convertor.ToFloat(stockData.Open) //openPrice, _ := convertor.ToFloat(stockData.Open)
if price > 0 && preClosePrice > 0 { if price > 0 {
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2) stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3) stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
} }
if highPrice > 0 && preClosePrice > 0 { if highPrice > 0 {
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3) stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
} }
if lowPrice > 0 && preClosePrice > 0 { if lowPrice > 0 {
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3) stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
} }
if follow.CostPrice > 0 && follow.Volume > 0 { if follow.CostPrice > 0 && follow.Volume > 0 {
@ -615,8 +261,7 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
//未开盘时当前价格为昨日收盘价 //未开盘时当前价格为昨日收盘价
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3) 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.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
// 未开盘时,今日盈亏为 0 stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = 0
} }
} }
@ -633,7 +278,6 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
// either by clicking the window close button or calling runtime.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. // Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) { func (a *App) beforeClose(ctx context.Context) (prevent bool) {
defer PanicHandler()
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{ dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog, Type: runtime.QuestionDialog,
@ -651,19 +295,14 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
logger.SugaredLogger.Debugf("dialog:%s", dialog) logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" { if dialog == "No" {
return true return true
} else {
systray.Quit()
a.cron.Stop()
return false
} }
return false
} }
// shutdown is called at application termination // shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) { func (a *App) shutdown(ctx context.Context) {
defer PanicHandler()
// Perform your teardown here // Perform your teardown here
//os.Exit(0) systray.Quit()
logger.SugaredLogger.Infof("application shutdown Version:%s", Version)
} }
// Greet returns a greeting for the given name // Greet returns a greeting for the given name
@ -673,7 +312,7 @@ func (a *App) Greet(stockCode string) *data.StockInfo {
follow := &data.FollowedStock{ follow := &data.FollowedStock{
StockCode: stockCode, StockCode: stockCode,
} }
db.Dao.Model(follow).Where("stock_code = ?", stockCode).Preload("Groups").Preload("Groups.GroupInfo").First(follow) db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
stockInfo := getStockInfo(*follow) stockInfo := getStockInfo(*follow)
return stockInfo return stockInfo
} }
@ -686,8 +325,8 @@ func (a *App) UnFollow(stockCode string) string {
return data.NewStockDataApi().UnFollow(stockCode) return data.NewStockDataApi().UnFollow(stockCode)
} }
func (a *App) GetFollowList(groupId int) *[]data.FollowedStock { func (a *App) GetFollowList() []data.FollowedStock {
return data.NewStockDataApi().GetFollowList(groupId) return data.NewStockDataApi().GetFollowList()
} }
func (a *App) GetStockList(key string) []data.StockBasic { func (a *App) GetStockList(key string) []data.StockBasic {
@ -720,19 +359,8 @@ func (a *App) SendDingDingMessage(message string, stockCode string) string {
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警 // SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string { 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)) ttl, _ := a.cache.TTL([]byte(stockCode))
//logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl) logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 { if ttl > 0 {
return "" return ""
} }
@ -747,68 +375,15 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
return data.NewDingDingAPI().SendDingDingMessage(message) return data.NewDingDingAPI().SendDingDingMessage(message)
} }
func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int) { func (a *App) NewChat(stock string) string {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId) return data.NewDeepSeekOpenAi().NewChat(stock)
}
func (a *App) NewChatStream(stock, stockCode string) {
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
for msg := range msgs { for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg) 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 { func GenNotificationMsg(stockInfo *data.StockInfo) string {
@ -857,298 +432,49 @@ func getMsgTypeName(msgType int) string {
func onExit(a *App) { func onExit(a *App) {
// 清理操作 // 清理操作
logger.SugaredLogger.Infof("systray onExit") logger.SugaredLogger.Infof("onExit")
//systray.Quit() runtime.Quit(a.ctx)
//runtime.Quit(a.ctx)
} }
func onReady(a *App) { func onReady(a *App) {
// 初始化操作 // 初始化操作
logger.SugaredLogger.Infof("systray onReady") logger.SugaredLogger.Infof("onReady")
systray.SetIcon(icon2) systray.SetIcon(icon2)
systray.SetTitle("go-stock") systray.SetTitle("go-stock")
systray.SetTooltip("go-stock 股票行情实时获取") systray.SetTooltip("go-stock 股票行情实时获取")
// 创建菜单项 // 创建菜单项
show := systray.AddMenuItem("显示", "显示应用程序") show := systray.AddMenuItem("显示", "显示应用程序")
show.Click(func() {
//logger.SugaredLogger.Infof("显示应用程序")
runtime.WindowShow(a.ctx)
})
hide := systray.AddMenuItem("隐藏", "隐藏应用程序") hide := systray.AddMenuItem("隐藏", "隐藏应用程序")
hide.Click(func() {
//logger.SugaredLogger.Infof("隐藏应用程序")
runtime.WindowHide(a.ctx)
})
systray.AddSeparator() systray.AddSeparator()
mQuitOrig := systray.AddMenuItem("退出", "退出应用程序") mQuitOrig := systray.AddMenuItem("退出", "退出应用程序")
mQuitOrig.Click(func() {
//logger.SugaredLogger.Infof("退出应用程序") // 监听菜单项点击事件
go func() {
for {
select {
case <-mQuitOrig.ClickedCh:
logger.SugaredLogger.Infof("退出应用程序")
runtime.Quit(a.ctx) runtime.Quit(a.ctx)
}) //systray.Quit()
systray.SetOnRClick(func(menu systray.IMenu) { case <-show.ClickedCh:
menu.ShowMenu() logger.SugaredLogger.Infof("显示应用程序")
//logger.SugaredLogger.Infof("SetOnRClick") runtime.WindowShow(a.ctx)
}) //runtime.WindowShow(a.ctx)
systray.SetOnClick(func(menu systray.IMenu) { case <-hide.ClickedCh:
//logger.SugaredLogger.Infof("SetOnClick") logger.SugaredLogger.Infof("隐藏应用程序")
menu.ShowMenu() runtime.WindowHide(a.ctx)
})
systray.SetOnDClick(func(menu systray.IMenu) { }
menu.ShowMenu() }
//logger.SugaredLogger.Infof("SetOnDClick") }()
})
} }
func (a *App) UpdateConfig(settings *data.Settings) string { func (a *App) UpdateConfig(settings *data.Settings) string {
//logger.SugaredLogger.Infof("UpdateConfig:%+v", settings) logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
if settings.RefreshInterval > 0 {
if entryID, exists := a.cronEntrys["MonitorStockPrices"]; exists {
a.cron.Remove(entryID)
}
id, _ := a.cron.AddFunc(fmt.Sprintf("@every %ds", settings.RefreshInterval), func() {
//logger.SugaredLogger.Infof("MonitorStockPrices:%s", time.Now())
MonitorStockPrices(a)
})
a.cronEntrys["MonitorStockPrices"] = id
}
return data.NewSettingsApi(settings).UpdateConfig() return data.NewSettingsApi(settings).UpdateConfig()
} }
func (a *App) GetConfig() *data.Settings { func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig() 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 (a *App) SaveAsMarkdown(stockCode, stockName string) string {
res := data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stockCode)
if res != nil && len(res.Content) > 100 {
analysisTime := res.CreatedAt.Format("2006-01-02_15_04_05")
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "保存为Markdown",
DefaultFilename: fmt.Sprintf("%s[%s]AI分析结果_%s.md", stockName, stockCode, analysisTime),
Filters: []runtime.FileFilter{
{
DisplayName: "Markdown",
Pattern: "*.md;*.markdown",
},
},
})
if err != nil {
return err.Error()
}
err = os.WriteFile(file, []byte(res.Content), 0644)
return "已保存至:" + file
}
return "分析结果异常,无法保存。"
}
func (a *App) GetPromptTemplates(name, promptType string) *[]models.PromptTemplate {
return data.NewPromptTemplateApi().GetPromptTemplates(name, promptType)
}
func (a *App) AddPrompt(prompt models.Prompt) string {
promptTemplate := models.PromptTemplate{
ID: prompt.ID,
Content: prompt.Content,
Name: prompt.Name,
Type: prompt.Type,
}
return data.NewPromptTemplateApi().AddPrompt(promptTemplate)
}
func (a *App) DelPrompt(id uint) string {
return data.NewPromptTemplateApi().DelPrompt(id)
}
func (a *App) SetStockAICron(cronText, stockCode string) {
data.NewStockDataApi().SetStockAICron(cronText, stockCode)
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
stockCode = strings.ToUpper(stockCode)
stockCode = strings.Replace(stockCode, "gb_", "us", 1)
stockCode = strings.Replace(stockCode, "GB_", "us", 1)
}
if entryID, exists := a.cronEntrys[stockCode]; exists {
a.cron.Remove(entryID)
}
follow := data.NewStockDataApi().GetFollowedStockByStockCode(stockCode)
id, _ := a.cron.AddFunc(cronText, a.AddCronTask(follow))
a.cronEntrys[stockCode] = id
}
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)
}
func (a *App) AddGroup(group data.Group) string {
ok := data.NewStockGroupApi(db.Dao).AddGroup(group)
if ok {
return "添加成功"
} else {
return "添加失败"
}
}
func (a *App) GetGroupList() []data.Group {
return data.NewStockGroupApi(db.Dao).GetGroupList()
}
func (a *App) GetGroupStockList(groupId int) []data.GroupStock {
return data.NewStockGroupApi(db.Dao).GetGroupStockByGroupId(groupId)
}
func (a *App) AddStockGroup(groupId int, stockCode string) string {
ok := data.NewStockGroupApi(db.Dao).AddStockGroup(groupId, stockCode)
if ok {
return "添加成功"
} else {
return "添加失败"
}
}
func (a *App) RemoveStockGroup(code, name string, groupId int) string {
ok := data.NewStockGroupApi(db.Dao).RemoveStockGroup(code, name, groupId)
if ok {
return "移除成功"
} else {
return "移除失败"
}
}
func (a *App) RemoveGroup(groupId int) string {
ok := data.NewStockGroupApi(db.Dao).RemoveGroup(groupId)
if ok {
return "移除成功"
} else {
return "移除失败"
}
}
func (a *App) GetStockKLine(stockCode, stockName string, days int64) *[]data.KLineData {
return data.NewStockDataApi().GetHK_KLineData(stockCode, "day", days)
}
func (a *App) GetStockMinutePriceLineData(stockCode, stockName string) map[string]any {
res := make(map[string]any, 4)
priceData, date := data.NewStockDataApi().GetStockMinutePriceData(stockCode)
res["priceData"] = priceData
res["date"] = date
res["stockName"] = stockName
res["stockCode"] = stockCode
return res
}
func (a *App) GetStockCommonKLine(stockCode, stockName string, days int64) *[]data.KLineData {
return data.NewStockDataApi().GetCommonKLineData(stockCode, "day", days)
}
func (a *App) GetTelegraphList(source string) *[]*models.Telegraph {
telegraphs := data.NewMarketNewsApi().GetTelegraphList(source)
return telegraphs
}
func (a *App) ReFleshTelegraphList(source string) *[]*models.Telegraph {
data.NewMarketNewsApi().GetNewTelegraph(30)
data.NewMarketNewsApi().GetSinaNews(30)
telegraphs := data.NewMarketNewsApi().GetTelegraphList(source)
return telegraphs
}
func (a *App) GlobalStockIndexes() map[string]any {
return data.NewMarketNewsApi().GlobalStockIndexes(30)
}
func (a *App) SummaryStockNews(question string, sysPromptId *int) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStream(question, sysPromptId)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "summaryStockNews", msg)
}
runtime.EventsEmit(a.ctx, "summaryStockNews", "DONE")
}
func (a *App) GetIndustryRank(sort string, cnt int) []any {
res := data.NewMarketNewsApi().GetIndustryRank(sort, cnt)
return res["data"].([]any)
}
func (a *App) GetIndustryMoneyRankSina(fenlei, sort string) []map[string]any {
res := data.NewMarketNewsApi().GetIndustryMoneyRankSina(fenlei, sort)
return res
}
func (a *App) GetMoneyRankSina(sort string) []map[string]any {
res := data.NewMarketNewsApi().GetMoneyRankSina(sort)
return res
}
func (a *App) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]any {
res := data.NewMarketNewsApi().GetStockMoneyTrendByDay(stockCode, days)
slice.Reverse(res)
return res
}

View File

@ -1,61 +0,0 @@
package main
import (
"go-stock/backend/data"
"go-stock/backend/models"
)
// @Author spark
// @Date 2025/6/8 20:45
// @Desc
//-----------------------------------------------------------------------------------
func (a *App) LongTigerRank(date string) *[]models.LongTigerRankData {
return data.NewMarketNewsApi().LongTiger(date)
}
func (a *App) StockResearchReport(stockCode string) []any {
return data.NewMarketNewsApi().StockResearchReport(stockCode, 7)
}
func (a *App) StockNotice(stockCode string) []any {
return data.NewMarketNewsApi().StockNotice(stockCode)
}
func (a *App) IndustryResearchReport(industryCode string) []any {
return data.NewMarketNewsApi().IndustryResearchReport(industryCode, 7)
}
func (a App) EMDictCode(code string) []any {
return data.NewMarketNewsApi().EMDictCode(code, a.cache)
}
func (a App) AnalyzeSentiment(text string) data.SentimentResult {
return data.AnalyzeSentiment(text)
}
func (a App) HotStock(marketType string) *[]models.HotItem {
return data.NewMarketNewsApi().XUEQIUHotStock(100, marketType)
}
func (a App) HotEvent(size int) *[]models.HotEvent {
if size <= 0 {
size = 10
}
return data.NewMarketNewsApi().HotEvent(size)
}
func (a App) HotTopic(size int) []any {
if size <= 0 {
size = 10
}
return data.NewMarketNewsApi().HotTopic(size)
}
func (a App) InvestCalendarTimeLine(yearMonth string) []any {
return data.NewMarketNewsApi().InvestCalendar(yearMonth)
}
func (a App) ClsCalendar() []any {
return data.NewMarketNewsApi().ClsCalendar()
}
func (a App) SearchStock(words string) map[string]any {
return data.NewSearchStockApi(words).SearchStock()
}

View File

@ -1,460 +0,0 @@
//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)
// 未开盘时,今日盈亏为 0
stockData.ProfitAmountToday = 0
}
}
//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()
}

View File

@ -1,25 +0,0 @@
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()))
}

View File

@ -1,52 +0,0 @@
//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
}

View File

@ -1,32 +0,0 @@
//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
}
}

View File

@ -31,7 +31,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
} }
func (a AlertWindowsApi) SendNotification() bool { func (a AlertWindowsApi) SendNotification() bool {
if GetConfig().LocalPushEnable == false { if getConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启") logger.SugaredLogger.Error("本地推送未开启")
return false return false
} }

View File

@ -1,237 +0,0 @@
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
pool *BrowserPool
}
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,
pool: NewBrowserPool(GetConfig().BrowserPoolSize),
}
}
func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bool) {
page, err := c.pool.FetchPage(url, waitVisible)
if err != nil {
return "", false
}
return page, true
}
func (c *CrawlerApi) GetHtml_old(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()
//defer chromedp.Cancel(ctx)
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()
//defer chromedp.Cancel(ctx)
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()
//defer chromedp.Cancel(ctx)
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()
//defer chromedp.Cancel(ctx)
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"`
}

View File

@ -1,371 +0,0 @@
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)
})
}
func TestSina(t *testing.T) {
db.Init("../../data/stock.db")
url := "https://finance.sina.com.cn/realstock/company/sz002906/nc.shtml"
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://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)
html, ok := crawlerAPI.GetHtml(url, "div#hqDetails table", true)
if !ok {
return
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
//price
price := strutil.RemoveWhiteSpace(document.Find("div#price").First().Text(), false)
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
var markdown strings.Builder
markdown.WriteString("\n ## 当前股票数据:\n")
markdown.WriteString(fmt.Sprintf("### 当前股价:%s 时间:%s\n", price, hqTime))
GetTableMarkdown(document, "div#hqDetails table", &markdown)
}
func TestDC(t *testing.T) {
url := "https://emweb.securities.eastmoney.com/pc_hsf10/pages/index.html?type=web&code=sh600745#/cwfx"
db.Init("../../data/stock.db")
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://emweb.securities.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)
var markdown strings.Builder
markdown.WriteString("\n ## 财务数据:\n")
html, ok := crawlerAPI.GetHtml(url, "div.report_table table", false)
if !ok {
return
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
GetTableMarkdown(document, "div.report_table table", &markdown)
}
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"`
}

View File

@ -21,8 +21,8 @@ func NewDingDingAPI() *DingDingAPI {
} }
func (DingDingAPI) SendDingDingMessage(message string) string { func (DingDingAPI) SendDingDingMessage(message string) string {
if GetConfig().DingPushEnable == false { if getConfig().DingPushEnable == false {
//logger.SugaredLogger.Info("钉钉推送未开启") logger.SugaredLogger.Info("钉钉推送未开启")
return "钉钉推送未开启" return "钉钉推送未开启"
} }
// 发送钉钉消息 // 发送钉钉消息
@ -37,11 +37,11 @@ func (DingDingAPI) SendDingDingMessage(message string) string {
logger.SugaredLogger.Infof("send dingding message: %s", resp.String()) logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
return "发送钉钉消息成功" return "发送钉钉消息成功"
} }
func GetConfig() *Settings { func getConfig() *Settings {
return NewSettingsApi(&Settings{}).GetConfig() return NewSettingsApi(&Settings{}).GetConfig()
} }
func getApiURL() string { func getApiURL() string {
return GetConfig().DingRobot return getConfig().DingRobot
} }
func (DingDingAPI) SendToDingDing(title, message string) string { func (DingDingAPI) SendToDingDing(title, message string) string {

View File

@ -1,401 +0,0 @@
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)
}
}
}

View File

@ -1,23 +0,0 @@
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")
}

View File

@ -1,703 +0,0 @@
package data
import (
"bytes"
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"github.com/robertkrimen/otto"
"github.com/samber/lo"
"github.com/tidwall/gjson"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strconv"
"strings"
"time"
)
// @Author spark
// @Date 2025/4/23 14:54
// @Desc
// -----------------------------------------------------------------------------------
type MarketNewsApi struct {
}
func NewMarketNewsApi() *MarketNewsApi {
return &MarketNewsApi{}
}
func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
url := "https://www.cls.cn/telegraph"
response, _ := 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))
var telegraphs []models.Telegraph
//logger.SugaredLogger.Info(string(response.Body()))
document, _ := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
document.Find(".telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph := models.Telegraph{Source: "财联社电报"}
spans := selection.Find("div.telegraph-content-box span")
if spans.Length() == 2 {
telegraph.Time = spans.First().Text()
telegraph.Content = spans.Last().Text()
if spans.Last().HasClass("c-de0422") {
telegraph.IsRed = true
}
}
labels := selection.Find("div a.label-item")
labels.Each(func(i int, selection *goquery.Selection) {
if selection.HasClass("link-label-item") {
telegraph.Url = selection.AttrOr("href", "")
} else {
tag := &models.Tags{
Name: selection.Text(),
Type: "subject",
}
db.Dao.Model(tag).Where("name=? and type=?", selection.Text(), "subject").FirstOrCreate(&tag)
telegraph.SubjectTags = append(telegraph.SubjectTags, selection.Text())
}
})
stocks := selection.Find("div.telegraph-stock-plate-box a")
stocks.Each(func(i int, selection *goquery.Selection) {
telegraph.StocksTags = append(telegraph.StocksTags, selection.Text())
})
//telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
if telegraph.Content != "" {
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
cnt := int64(0)
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
for _, tag := range telegraph.SubjectTags {
tagInfo := &models.Tags{}
db.Dao.Model(models.Tags{}).Where("name=? and type=?", tag, "subject").First(&tagInfo)
if tagInfo.ID > 0 {
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tagInfo.ID).FirstOrCreate(&models.TelegraphTags{
TelegraphId: telegraph.ID,
TagId: tagInfo.ID,
})
}
}
}
}
})
return &telegraphs
}
func (m MarketNewsApi) GetNewsList(source string, limit int) *[]*models.Telegraph {
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(limit).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(limit).Find(news)
}
for _, item := range *news {
tags := &[]models.Tags{}
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
return item.TagId
})).Find(&tags)
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
return item.Name
})
item.SubjectTags = tagNames
logger.SugaredLogger.Infof("tagNames %v SubjectTags%s", tagNames, item.SubjectTags)
}
return news
}
func (m MarketNewsApi) GetTelegraphList(source string) *[]*models.Telegraph {
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(20).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(20).Find(news)
}
for _, item := range *news {
tags := &[]models.Tags{}
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
return item.TagId
})).Find(&tags)
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
return item.Name
})
item.SubjectTags = tagNames
logger.SugaredLogger.Infof("tagNames %v SubjectTags%s", tagNames, item.SubjectTags)
}
return news
}
func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
news := &[]models.Telegraph{}
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
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/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get("https://zhibo.sina.com.cn/api/zhibo/feed?callback=callback&page=1&page_size=20&zhibo_id=152&tag_id=0&dire=f&dpc=1&pagesize=20&id=4161089&type=0&_=" + strconv.FormatInt(time.Now().Unix(), 10))
js := string(response.Body())
js = strutil.ReplaceWithMap(js, map[string]string{
"try{callback(": "var data=",
");}catch(e){};": ";",
})
//logger.SugaredLogger.Info(js)
vm := otto.New()
_, err := vm.Run(js)
if err != nil {
logger.SugaredLogger.Error(err)
}
vm.Run("var result = data.result;")
//vm.Run("var resultStr =JSON.stringify(data);")
vm.Run("var resultData = result.data;")
vm.Run("var feed = resultData.feed;")
vm.Run("var feedStr = JSON.stringify(feed);")
value, _ := vm.Get("feedStr")
//resultStr, _ := vm.Get("resultStr")
//logger.SugaredLogger.Info(resultStr)
feed := make(map[string]any)
err = json.Unmarshal([]byte(value.String()), &feed)
if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
var telegraphs []models.Telegraph
if feed["list"] != nil {
for _, item := range feed["list"].([]any) {
telegraph := models.Telegraph{Source: "新浪财经"}
data := item.(map[string]any)
//logger.SugaredLogger.Infof("%s:%s", data["create_time"], data["rich_text"])
telegraph.Content = data["rich_text"].(string)
telegraph.Time = strings.Split(data["create_time"].(string), " ")[1]
tags := data["tag"].([]any)
telegraph.SubjectTags = lo.Map(tags, func(tagItem any, index int) string {
name := tagItem.(map[string]any)["name"].(string)
tag := &models.Tags{
Name: name,
Type: "sina_subject",
}
db.Dao.Model(tag).Where("name=? and type=?", name, "sina_subject").FirstOrCreate(&tag)
return name
})
if _, ok := lo.Find(telegraph.SubjectTags, func(item string) bool { return item == "焦点" }); ok {
telegraph.IsRed = true
}
logger.SugaredLogger.Infof("telegraph.SubjectTags:%v %s", telegraph.SubjectTags, telegraph.Content)
if telegraph.Content != "" {
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
cnt := int64(0)
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
for _, tag := range telegraph.SubjectTags {
tagInfo := &models.Tags{}
db.Dao.Model(models.Tags{}).Where("name=? and type=?", tag, "sina_subject").First(&tagInfo)
if tagInfo.ID > 0 {
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tagInfo.ID).FirstOrCreate(&models.TelegraphTags{
TelegraphId: telegraph.ID,
TagId: tagInfo.ID,
})
}
}
}
}
}
return &telegraphs
}
return news
}
func (m MarketNewsApi) GlobalStockIndexes(crawlTimeOut uint) map[string]any {
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://stockapp.finance.qq.com/mstats").
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("https://proxy.finance.qq.com/ifzqgtimg/appstock/app/rank/indexRankDetail2")
js := string(response.Body())
res := make(map[string]any)
json.Unmarshal([]byte(js), &res)
return res["data"].(map[string]any)
}
func (m MarketNewsApi) GetIndustryRank(sort string, cnt int) map[string]any {
url := fmt.Sprintf("https://proxy.finance.qq.com/ifzqgtimg/appstock/app/mktHs/rank?l=%d&p=1&t=01/averatio&ordertype=&o=%s", cnt, sort)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Referer", "https://stockapp.finance.qq.com/").
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(url)
js := string(response.Body())
res := make(map[string]any)
json.Unmarshal([]byte(js), &res)
return res
}
func (m MarketNewsApi) GetIndustryMoneyRankSina(fenlei, sort string) []map[string]any {
url := fmt.Sprintf("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_bkzj_bk?page=1&num=20&sort=%s&asc=0&fenlei=%s", sort, fenlei)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.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/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(url)
js := string(response.Body())
res := &[]map[string]any{}
err := json.Unmarshal([]byte(js), &res)
if err != nil {
logger.SugaredLogger.Error(err)
return *res
}
return *res
}
func (m MarketNewsApi) GetMoneyRankSina(sort string) []map[string]any {
if sort == "" {
sort = "netamount"
}
url := fmt.Sprintf("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_bkzj_ssggzj?page=1&num=20&sort=%s&asc=0&bankuai=&shichang=", sort)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.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/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(url)
js := string(response.Body())
res := &[]map[string]any{}
err := json.Unmarshal([]byte(js), &res)
if err != nil {
logger.SugaredLogger.Error(err)
return *res
}
return *res
}
func (m MarketNewsApi) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]any {
url := fmt.Sprintf("http://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_qsfx_zjlrqs?page=1&num=%d&sort=opendate&asc=0&daima=%s", days, stockCode)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.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/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").Get(url)
js := string(response.Body())
res := &[]map[string]any{}
err := json.Unmarshal([]byte(js), &res)
if err != nil {
logger.SugaredLogger.Error(err)
return *res
}
return *res
}
func (m MarketNewsApi) TopStocksRankingList(date string) {
url := fmt.Sprintf("http://vip.stock.finance.sina.com.cn/q/go.php/vInvestConsult/kind/lhb/index.phtml?tradedate=%s", date)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.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/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").Get(url)
html, _ := convertor.GbkToUtf8(response.Body())
//logger.SugaredLogger.Infof("html:%s", html)
document, err := goquery.NewDocumentFromReader(bytes.NewReader(html))
if err != nil {
return
}
document.Find("table.list_table").Each(func(i int, s *goquery.Selection) {
title := strutil.Trim(s.Find("tr:first-child").First().Text())
logger.SugaredLogger.Infof("title:%s", title)
s.Find("tr:not(:first-child)").Each(func(i int, s *goquery.Selection) {
logger.SugaredLogger.Infof("s:%s", strutil.RemoveNonPrintable(s.Text()))
})
})
}
func (m MarketNewsApi) LongTiger(date string) *[]models.LongTigerRankData {
ranks := &[]models.LongTigerRankData{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get"
logger.SugaredLogger.Infof("url:%s", url)
params := make(map[string]string)
params["callback"] = "callback"
params["sortColumns"] = "TURNOVERRATE,TRADE_DATE,SECURITY_CODE"
params["sortTypes"] = "-1,-1,1"
params["pageSize"] = "500"
params["pageNumber"] = "1"
params["reportName"] = "RPT_DAILYBILLBOARD_DETAILSNEW"
params["columns"] = "SECURITY_CODE,SECUCODE,SECURITY_NAME_ABBR,TRADE_DATE,EXPLAIN,CLOSE_PRICE,CHANGE_RATE,BILLBOARD_NET_AMT,BILLBOARD_BUY_AMT,BILLBOARD_SELL_AMT,BILLBOARD_DEAL_AMT,ACCUM_AMOUNT,DEAL_NET_RATIO,DEAL_AMOUNT_RATIO,TURNOVERRATE,FREE_MARKET_CAP,EXPLANATION,D1_CLOSE_ADJCHRATE,D2_CLOSE_ADJCHRATE,D5_CLOSE_ADJCHRATE,D10_CLOSE_ADJCHRATE,SECURITY_TYPE_CODE"
params["source"] = "WEB"
params["client"] = "WEB"
params["filter"] = fmt.Sprintf("(TRADE_DATE<='%s')(TRADE_DATE>='%s')", date, date)
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/stock/tradedetail.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetQueryParams(params).
Get(url)
if err != nil {
return ranks
}
js := string(resp.Body())
logger.SugaredLogger.Infof("resp:%s", js)
js = strutil.ReplaceWithMap(js, map[string]string{
"callback(": "var data=",
");": ";",
})
//logger.SugaredLogger.Info(js)
vm := otto.New()
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(data);")
value, err := vm.Get("data")
logger.SugaredLogger.Infof("resp-json:%s", value.String())
data := gjson.Get(value.String(), "result.data")
logger.SugaredLogger.Infof("resp:%v", data)
err = json.Unmarshal([]byte(data.String()), ranks)
if err != nil {
logger.SugaredLogger.Error(err)
return ranks
}
for _, rankData := range *ranks {
temp := &models.LongTigerRankData{}
db.Dao.Model(temp).Where(&models.LongTigerRankData{
TRADEDATE: rankData.TRADEDATE,
SECUCODE: rankData.SECUCODE,
}).First(temp)
if temp.SECURITYTYPECODE == "" {
db.Dao.Model(temp).Create(&rankData)
}
}
return ranks
}
func (m MarketNewsApi) IndustryResearchReport(industryCode string, days int) []any {
beginDate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).Format("2006-01-02")
endDate := time.Now().Format("2006-01-02")
if strutil.Trim(industryCode) != "" {
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
}
logger.SugaredLogger.Infof("IndustryResearchReport-name:%s", industryCode)
params := map[string]string{
"industry": "*",
"industryCode": industryCode,
"beginTime": beginDate,
"endTime": endDate,
"pageNo": "1",
"pageSize": "50",
"p": "1",
"pageNum": "1",
"pageNumber": "1",
"qType": "1",
}
url := "https://reportapi.eastmoney.com/report/list"
logger.SugaredLogger.Infof("beginDate:%s endDate:%s", beginDate, endDate)
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "reportapi.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/stock.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetQueryParams(params).Get(url)
respMap := map[string]any{}
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap["data"].([]any)
}
func (m MarketNewsApi) StockResearchReport(stockCode string, days int) []any {
beginDate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).Format("2006-01-02")
endDate := time.Now().Format("2006-01-02")
if strutil.ContainsAny(stockCode, []string{"."}) {
stockCode = strings.Split(stockCode, ".")[0]
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
} else {
stockCode = strutil.ReplaceWithMap(stockCode, map[string]string{
"sh": "",
"sz": "",
"gb_": "",
"us": "",
"us_": "",
})
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
}
logger.SugaredLogger.Infof("StockResearchReport-stockCode:%s", stockCode)
type Req struct {
BeginTime string `json:"beginTime"`
EndTime string `json:"endTime"`
IndustryCode string `json:"industryCode"`
RatingChange string `json:"ratingChange"`
Rating string `json:"rating"`
OrgCode interface{} `json:"orgCode"`
Code string `json:"code"`
Rcode string `json:"rcode"`
PageSize int `json:"pageSize"`
PageNo int `json:"pageNo"`
P int `json:"p"`
PageNum int `json:"pageNum"`
PageNumber int `json:"pageNumber"`
}
url := "https://reportapi.eastmoney.com/report/list2"
logger.SugaredLogger.Infof("beginDate:%s endDate:%s", beginDate, endDate)
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "reportapi.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/stock.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetBody(&Req{
Code: stockCode,
IndustryCode: "*",
BeginTime: beginDate,
EndTime: endDate,
PageNo: 1,
PageSize: 50,
P: 1,
PageNum: 1,
PageNumber: 1,
}).Post(url)
respMap := map[string]any{}
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap["data"].([]any)
}
func (m MarketNewsApi) StockNotice(stock_list string) []any {
var stockCodes []string
for _, stockCode := range strings.Split(stock_list, ",") {
if strutil.ContainsAny(stockCode, []string{"."}) {
stockCode = strings.Split(stockCode, ".")[0]
stockCodes = append(stockCodes, stockCode)
} else {
stockCode = strutil.ReplaceWithMap(stockCode, map[string]string{
"sh": "",
"sz": "",
"gb_": "",
"us": "",
"us_": "",
})
stockCodes = append(stockCodes, stockCode)
}
}
url := "https://np-anotice-stock.eastmoney.com/api/security/ann?page_size=50&page_index=1&ann_type=SHA%2CCYB%2CSZA%2CBJA%2CINV&client_source=web&f_node=0&stock_list=" + strings.Join(stockCodes, ",")
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "np-anotice-stock.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/notices/hsa/5.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
respMap := map[string]any{}
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return (respMap["data"].(map[string]any))["list"].([]any)
}
func (m MarketNewsApi) EMDictCode(code string, cache *freecache.Cache) []any {
respMap := map[string]any{}
d, _ := cache.Get([]byte(code))
if d != nil {
json.Unmarshal(d, &respMap)
return respMap["data"].([]any)
}
url := "https://reportapi.eastmoney.com/report/bk"
params := map[string]string{
"bkCode": code,
}
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "reportapi.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/industry.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetQueryParams(params).Get(url)
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
cache.Set([]byte(code), resp.Body(), 60*60*24)
return respMap["data"].([]any)
}
func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
TVNews := &[]models.TVNews{}
url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang:zh-Hans&filter=provider:panews,reuters&client=screener&streaming=false"
resp, err := resty.New().SetProxy("http://127.0.0.1:10809").SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "news-mediator.tradingview.com").
SetHeader("Origin", "https://cn.tradingview.com").
SetHeader("Referer", "https://cn.tradingview.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("TradingViewNews err:%s", err.Error())
return TVNews
}
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
if err != nil {
return TVNews
}
items, err := json.Marshal(respMap["items"])
if err != nil {
return TVNews
}
json.Unmarshal(items, TVNews)
return TVNews
}
func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.HotItem {
request := resty.New().SetTimeout(time.Duration(30) * time.Second).R()
_, err := request.
SetHeader("Host", "xueqiu.com").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get("https://xueqiu.com/hq#hot")
//cookies := resp.Header().Get("Set-Cookie")
//logger.SugaredLogger.Infof("cookies:%s", cookies)
url := fmt.Sprintf("https://stock.xueqiu.com/v5/stock/hot_stock/list.json?page=1&size=%d&_type=%s&type=%s", size, marketType, marketType)
res := &models.XUEQIUHot{}
_, err = request.
SetHeader("Host", "stock.xueqiu.com").
SetHeader("Origin", "https://xueqiu.com").
SetHeader("Referer", "https://xueqiu.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
//SetHeader("Cookie", "cookiesu=871730774144180; device_id=ee75cebba8a35005c9e7baf7b7dead59; s=ch12b12pfi; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746247619; xq_a_token=361dcfccb1d32a1d9b5b65f1a188b9c9ed1e687d; xqat=361dcfccb1d32a1d9b5b65f1a188b9c9ed1e687d; xq_r_token=450d1db0db9659a6af7cc9297bfa4fccf1776fae; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MzgzODAwNiwiY3RtIjoxNzUxMjUxMzc2MDY3LCJjaWQiOiJkOWQwbjRBWnVwIn0.TjEtQ5WEN4ajnVjVnY3J-Qq9LjL-F0eat9Cefv_tLJLqsPhzD2y8Lc1CeIu0Ceqhlad7O_yW1tR9nb2dIjDpyOPzWKxvwSOKXLm8XMoz4LMgE2pysBCH4TsetzHsEOhBsY467q-JX3WoFuqo-dqv1FfLSondZCspjEMFdgPFt2V-2iXJY05YUwcBVUvL74mT9ZjNq0KaDeRBJk_il6UR8yibG7RMbe9xWYz5dSO_wJwWuxvnZ8u9EXC2m-TV7-QHVxFHR_5e8Fodrzg0yIcLU4wBTSoIIQDUKqngajX2W-nUAdo6fr78NNDmoswFVH7T7XMuQciMAqj9MpMCVW3Sog; u=871730774144180; ssxmod_itna=iq+h7KAImDORKYQ4Y5G=nxBKDtD7D3qCD0dGMDxeq7tDRDFqApKDHtA68oon7ziBA0+PbZ9xGN4oYxiNDAPq0iDC+Wjxs9Orw5KQb9iqP4MAn0TbNsbtU22eqbCe=S3vTv6xoDHxY=DU1GzeieDx=PD5xDTDWeDGDD3DmnsDi5YD0KDjBYpH+omDYPDEBYDaxDbDimwY4GCrDDCtc5Dw6bmzDDzznL5WWAPzWffZg3YcFgxf8GwD7y3Dla4rMhw23=cz0Efdk0A5hYDXotDvhoY1/H6neEvOt3o=Q0ruT+5RuxoRhDxCmh5tGP32xBD5G0xS2xcb4quDK0Dy2ZmY/DDWM0qmEeSEDeOCIq1fw1misCY=WAzoOtMwDzGdUjpRk5Z0xQBDI2IMw4H7qNiNBLxWiDD; ssxmod_itna2=iq+h7KAImDORKYQ4Y5G=nxBKDtD7D3qCD0dGMDxeq7tDRDFqApKDHtA68oon7ziBA0+PbZYxD3boBmiEPtDFOEPAeFmDDsuGSxf46oGKwGHd8wtUjFe+oV1lxUzutkGly=nCyCjq=UTHxMxFCr1DsFiKPuEpPVO7GrOyk5Aymnc0+11AFND7v16PvwrFQH4I72=3O1OpK7rGw+poWNCxjj=Ka5QDFWAvEzrDFQcIH=GpKpS90FAyIzGcTyck+yhQKaojn96dRqeIh=HkaFrlGnKwzO+a49=F7/c/MejoR3QM20K9IIOymrMN2bsk2TRdKFiaf4O0ut2MauiOER=iQNW2WVgDrkKzD=57r577wEx2hwkqhf8T8BDvkHZRDirC0bNK4O=G3TSkd3wYwq8bst0t9qF/e3M87NYtU2IWYWzqd=BqEfdqGq0R8wxmqLzpeGeuwSTq1OAiB87gDrozjnGkwDKRdrLz8uDjQKVlGhWk8Wd/rXQjx4pG=BNqpW/6TS1wpfxzGf5CrUhtt0j0wC5AUFo2GbX+QXPzD2guxKXrx8lZUQlwWIHyEUz+OLh0eWUkfHfM0YWXlgOejnuUa06rW9y5maDPipGms751hxKcqLq62pQty4iX3QDF6SRQd3tfEBf3CH7r2xe2qq0qdOI5Ge=GezD/Us5Z0xQBwVAZ2N/XvD0HDD").
SetResult(res).
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error())
return &[]models.HotItem{}
}
logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
return &res.Data.Items
}
func (m MarketNewsApi) HotEvent(size int) *[]models.HotEvent {
events := &[]models.HotEvent{}
url := fmt.Sprintf("https://xueqiu.com/hot_event/list.json?count=%d", size)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "xueqiu.com").
SetHeader("Origin", "https://xueqiu.com").
SetHeader("Referer", "https://xueqiu.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Cookie", "cookiesu=2617378771242871; s=c2121pp1u71; device_id=237a58584ec58d8e4d4e1040700a644f1; Hm_lvt_1db88642e346389874251b5a1eded6e3=1744100219,1744599115; xq_a_token=b7259d09435458cc3f1a963479abb270a1a016ce; xqat=b7259d09435458cc3f1a963479abb270a1a016ce; xq_r_token=28108bfa1d92ac8a46bbb57722633746218621a3; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MjU0MTk4OCwiY3RtIjoxNzUwMjMwNjA2NzI0LCJjaWQiOiJkOWQwbjRBWnVwIn0.kU_fz0luJoE7nr-K4UrNUi5-mAG-vMdXtuC4mUKIppILId4UpF70LB70yunxGiNSw6tPFR3-hyLvztKAHtekCUTm3XjUl5b3tEDP-ZUVqHnWXO_5hoeMI8h-Cfx6ZGlIr5x3icvTPkT0OV5CD5A33-ZDTKhKPf-DhJ_-m7CG5GbX4MseOBeMXuLUQUiYHPKhX1QUc0GTGrCzi8Mki0z49D0LVqCSgbsx3UGfowOOyx85_cXb4OAFvIjwbs2p0o_h-ibIT0ngVkkAyEDetVvlcZ_bkardhseCB7k9BEMgH2z8ihgkVxyy3P0degLmDUruhmqn5uZOCi1pVBDvCv9lBg; u=261737877124287; ssxmod_itna=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqEeDBubrDSxD67DK4GTm+ogiw1o3B=xedQHDgBtN=7/i1K53N+rOjquLMU=kbqYxB3DExGkqj0tPi4DxaPD5xDTDWeDGDD3DnnsDQKDRx0kL0oDIxD1D0bmHUEvh38mDYePLmOmDYPYx94Y8KoDeEgsD7HUl/vIGGEAqjLPFegXLD0HolCqr4DCid1qDm+ECfkjDn9sD0KP8fn+CRoDv=tYr4ibx+o=W+8vstf9mjGe3cXseWdBmoFrmf4DA3bFAxnAxD7vYxADaDoerDGHPoxHF+PKGPtDKmiqQGeB5qbi4eg4KDHKDe3DeG0qeEP9xVUoHDDWMYYM0ICr4FBimBDM7D0x4QOECmhul5QCN/m5/74lGm=7x9Wp7A+i7xQ7wlMD4D; ssxmod_itna2=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqoDirSDhPmGD24GajjDuGE3m7or4DlxOSGewHl6iaus2Q62SRX5CFjCds6ltF9xy6iaUuB262UkhRA8UXST=4/b+y3kGKzlGE8T29FA008ljy9jXXC7f7m7QsK667mlUooWrofk=qGZjxtcUrN1NtuAnne1hj+rQP5UnlFkxf+o7VjmatH7u7bCDlbTt3cz6CH9Fl4vye16W/ellc8I3Q37W7ZwiLGD/zPpZcnd2nsqqo/+zRbKAmz4plzwaDqGUe7f9E+P0IFRKqpRv+buQFHBSpcbwND7Q+9XWmnjI2UwKd98jIS3gPXwxvbx4OuiyH8gZ+OEt7DgE/AY/9W4VxDZrlFWyWnC4y4/I0IpAfaGKpbPmauKbkqawqv93vSf+9HamGe0Dt2PNgT3yiEB4vQP2/DdVpcGBOjFujWoHP32OshLPYI20LRCKddwEGkKqPzPwKPc3X5zuB=w2fUdtwKsAW5kQtsl8clNwjC5uDYrxR0h9xaj0xmD+YuI3GPT7xYTalRImPj2wL2=+91a304xa4bTWtP=dLGARhb/efRi0uktaz8i8C04G0x/ZWUzqRza8GGU=FfRfvb4GZM/q2rVsl0nLvRjGeAKgocLouyXs/uwZu3YxbAx30qCbjG1A533zAxIeIgD=0VAc3ixD").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("HotEvent err:%s", err.Error())
return events
}
//logger.SugaredLogger.Infof("HotEvent:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
items, err := json.Marshal(respMap["list"])
if err != nil {
return events
}
json.Unmarshal(items, events)
return events
}
func (m MarketNewsApi) HotTopic(size int) []any {
url := "https://gubatopic.eastmoney.com/interface/GetData.aspx?path=newtopic/api/Topic/HomePageListRead"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "gubatopic.eastmoney.com").
SetHeader("Origin", "https://gubatopic.eastmoney.com").
SetHeader("Referer", "https://gubatopic.eastmoney.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetFormData(map[string]string{
"param": fmt.Sprintf("ps=%d&p=1&type=0", size),
"path": "newtopic/api/Topic/HomePageListRead",
"env": "2",
}).
Post(url)
if err != nil {
logger.SugaredLogger.Errorf("HotTopic err:%s", err.Error())
return []any{}
}
//logger.SugaredLogger.Infof("HotTopic:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["re"].([]any)
}
func (m MarketNewsApi) InvestCalendar(yearMonth string) []any {
if yearMonth == "" {
yearMonth = time.Now().Format("2006-01")
}
url := "https://app.jiuyangongshe.com/jystock-app/api/v1/timeline/list"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "app.jiuyangongshe.com").
SetHeader("Origin", "https://www.jiuyangongshe.com").
SetHeader("Referer", "https://www.jiuyangongshe.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetHeader("token", "1cc6380a05c652b922b3d85124c85473").
SetHeader("platform", "3").
SetHeader("Cookie", "SESSION=NDZkNDU2ODYtODEwYi00ZGZkLWEyY2ItNjgxYzY4ZWMzZDEy").
SetHeader("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)).
SetBody(map[string]string{
"date": yearMonth,
"grade": "0",
}).
Post(url)
if err != nil {
logger.SugaredLogger.Errorf("InvestCalendar err:%s", err.Error())
return []any{}
}
//logger.SugaredLogger.Infof("InvestCalendar:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["data"].([]any)
}
func (m MarketNewsApi) ClsCalendar() []any {
url := "https://www.cls.cn/api/calendar/web/list?app=CailianpressWeb&flag=0&os=web&sv=8.4.6&type=0&sign=4b839750dc2f6b803d1c8ca00d2b40be"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "www.cls.cn").
SetHeader("Origin", "https://www.cls.cn").
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("ClsCalendar err:%s", err.Error())
return []any{}
}
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["data"].([]any)
}

View File

@ -1,153 +0,0 @@
package data
import (
"encoding/json"
"github.com/coocood/freecache"
"go-stock/backend/db"
"go-stock/backend/logger"
"testing"
)
// @Author spark
// @Date 2025/4/23 17:58
// @Desc
//-----------------------------------------------------------------------------------
func TestGetSinaNews(t *testing.T) {
db.Init("../../data/stock.db")
NewMarketNewsApi().GetSinaNews(30)
//NewMarketNewsApi().GetNewTelegraph(30)
}
func TestGlobalStockIndexes(t *testing.T) {
resp := NewMarketNewsApi().GlobalStockIndexes(30)
bytes, err := json.Marshal(resp)
if err != nil {
return
}
logger.SugaredLogger.Debugf("resp: %+v", string(bytes))
}
func TestGetIndustryRank(t *testing.T) {
res := NewMarketNewsApi().GetIndustryRank("0", 10)
for s, a := range res["data"].([]any) {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", s, a)
}
}
func TestGetIndustryMoneyRankSina(t *testing.T) {
res := NewMarketNewsApi().GetIndustryMoneyRankSina("0", "netamount")
for i, re := range res {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
}
}
func TestGetMoneyRankSina(t *testing.T) {
res := NewMarketNewsApi().GetMoneyRankSina("r3_net")
for i, re := range res {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
}
}
func TestGetStockMoneyTrendByDay(t *testing.T) {
res := NewMarketNewsApi().GetStockMoneyTrendByDay("sh600438", 360)
for i, re := range res {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
}
}
func TestTopStocksRankingList(t *testing.T) {
NewMarketNewsApi().TopStocksRankingList("2025-05-19")
}
func TestLongTiger(t *testing.T) {
db.Init("../../data/stock.db")
NewMarketNewsApi().LongTiger("2025-06-08")
}
func TestStockResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().StockResearchReport("600584.sh", 7)
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestIndustryResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().IndustryResearchReport("", 7)
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestStockNotice(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().StockNotice("600584,600900")
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestEMDictCode(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().EMDictCode("016", freecache.NewCache(100))
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestTradingViewNews(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().TradingViewNews()
for _, a := range *resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestXUEQIUHotStock(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().XUEQIUHotStock(50, "10")
for _, a := range *res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestHotEvent(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().HotEvent(50)
for _, a := range *res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestHotTopic(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().HotTopic(10)
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestInvestCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().InvestCalendar("2025-06")
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestClsCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().ClsCalendar()
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,21 @@
package data package data
import ( import (
"context"
"go-stock/backend/db" "go-stock/backend/db"
"testing" "testing"
) )
func TestNewDeepSeekOpenAiConfig(t *testing.T) { func TestNewDeepSeekOpenAiConfig(t *testing.T) {
db.Init("../../data/stock.db") db.Init("../../data/stock.db")
ai := NewDeepSeekOpenAi(context.TODO()) ai := NewDeepSeekOpenAi()
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil) res := ai.NewChatStream("闻泰科技", "sh600745")
for { for {
select { select {
case msg := <-res: case msg := <-res:
if msg == "" {
return
}
t.Log(msg) t.Log(msg)
} }
} }
} }
func TestGetTopNewsList(t *testing.T) {
news := GetTopNewsList(30)
t.Log(news)
}
func TestSearchGuShiTongStockInfo(t *testing.T) {
db.Init("../../data/stock.db")
SearchGuShiTongStockInfo("hk01810", 60)
SearchGuShiTongStockInfo("sh600745", 60)
SearchGuShiTongStockInfo("gb_goog", 60)
}

View File

@ -1,115 +0,0 @@
package data
import (
"context"
"go-stock/backend/logger"
"sync"
"time"
"github.com/chromedp/chromedp"
)
// BrowserPool 浏览器池结构
type BrowserPool struct {
pool chan *context.Context
mu sync.Mutex
size int
}
// NewBrowserPool 创建新的浏览器池
func NewBrowserPool(size int) *BrowserPool {
pool := make(chan *context.Context, size)
for i := 0; i < size; i++ {
path := GetConfig().BrowserPath
crawlTimeOut := GetConfig().CrawlTimeOut
if crawlTimeOut < 15 {
crawlTimeOut = 30
}
if path != "" {
ctx, _ := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
ctx, _ = chromedp.NewExecAllocator(
ctx,
chromedp.ExecPath(path),
chromedp.Flag("headless", true),
chromedp.Flag("blink-settings", "imagesEnabled=false"),
chromedp.Flag("disable-javascript", false),
chromedp.Flag("disable-gpu", true),
//chromedp.UserAgent(""),
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),
)
ctx, _ = chromedp.NewContext(ctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
pool <- &ctx
}
}
return &BrowserPool{
pool: pool,
size: size,
}
}
// Get 从池中获取浏览器实例
func (pool *BrowserPool) Get() *context.Context {
return <-pool.pool
}
// Put 将浏览器实例放回池中
func (pool *BrowserPool) Put(ctx *context.Context) {
pool.mu.Lock()
defer pool.mu.Unlock()
// 检查池是否已满
if len(pool.pool) >= pool.size {
// 池已满,关闭并丢弃这个实例
chromedp.Cancel(*ctx)
return
}
chromedp.Cancel(*ctx)
pool.pool <- ctx
}
// Close 关闭池中的所有浏览器实例
func (pool *BrowserPool) Close() {
close(pool.pool)
for ctx := range pool.pool {
chromedp.Cancel(*ctx)
}
}
// FetchPage 使用浏览器池获取页面内容
func (pool *BrowserPool) FetchPage(url, waitVisible string) (string, error) {
// 从池中获取浏览器实例
ctx := pool.Get()
defer pool.Put(ctx) // 使用完毕后放回池中
var htmlContent string
err := chromedp.Run(*ctx,
chromedp.Navigate(url),
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
chromedp.InnerHTML("body", &htmlContent),
chromedp.Evaluate(`window.close()`, nil),
)
if err != nil {
return "", err
}
return htmlContent, nil
}

View File

@ -1,18 +0,0 @@
package data
import (
"go-stock/backend/db"
"testing"
)
func TestPool(t *testing.T) {
db.Init("../../data/stock.db")
pool := NewBrowserPool(1)
go pool.FetchPage("https://fund.eastmoney.com/016533.html", "body")
go pool.FetchPage("https://fund.eastmoney.com/217021.html", "body")
go pool.FetchPage("https://fund.eastmoney.com/001125.html", "body")
select {}
}

View File

@ -1,75 +0,0 @@
package data
import (
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
)
type PromptTemplateApi struct {
}
func (t PromptTemplateApi) GetPromptTemplates(name string, promptType string) *[]models.PromptTemplate {
var result []models.PromptTemplate
if name != "" && promptType != "" {
db.Dao.Model(&models.PromptTemplate{}).Where("name=? and type=?", name, promptType).Find(&result)
}
if name != "" && promptType == "" {
db.Dao.Model(&models.PromptTemplate{}).Where("name=?", name).Find(&result)
}
if name == "" && promptType != "" {
db.Dao.Model(&models.PromptTemplate{}).Where("type=?", promptType).Find(&result)
}
if name == "" && promptType == "" {
db.Dao.Model(&models.PromptTemplate{}).Find(&result)
}
return &result
}
func (t PromptTemplateApi) AddPrompt(template models.PromptTemplate) string {
var tmp models.PromptTemplate
db.Dao.Model(&models.PromptTemplate{}).Where("id=?", template.ID).First(&tmp)
if tmp.ID == 0 {
err := db.Dao.Model(&models.PromptTemplate{}).Create(&models.PromptTemplate{
Content: template.Content,
Name: template.Name,
Type: template.Type,
}).Error
if err != nil {
return "添加失败"
} else {
return "添加成功"
}
} else {
err := db.Dao.Model(&models.PromptTemplate{}).Where("id=?", template.ID).Updates(template).Error
if err != nil {
return "更新失败"
} else {
return "更新成功"
}
}
}
func (t PromptTemplateApi) DelPrompt(Id uint) string {
template := &models.PromptTemplate{}
db.Dao.Model(template).Where("id=?", Id).Find(template)
if template.ID > 0 {
err := db.Dao.Model(template).Delete(template).Error
if err != nil {
return "删除失败"
} else {
return "删除成功"
}
}
return "模板信息不存在"
}
func (t PromptTemplateApi) GetPromptTemplateByID(id int) string {
prompt := &models.PromptTemplate{}
db.Dao.Model(&models.PromptTemplate{}).Where("id=?", id).First(prompt)
logger.SugaredLogger.Infof("GetPromptTemplateByID:%d %s", id, prompt.Content)
return prompt.Content
}
func NewPromptTemplateApi() *PromptTemplateApi {
return &PromptTemplateApi{}
}

View File

@ -1,55 +0,0 @@
package data
import (
"encoding/json"
"fmt"
"github.com/go-resty/resty/v2"
"go-stock/backend/logger"
"time"
)
// @Author spark
// @Date 2025/6/28 21:02
// @Desc
// -----------------------------------------------------------------------------------
type SearchStockApi struct {
words string
}
func NewSearchStockApi(words string) *SearchStockApi {
return &SearchStockApi{words: words}
}
func (s SearchStockApi) SearchStock() map[string]any {
url := "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "np-tjxg-g.eastmoney.com").
SetHeader("Origin", "https://xuangu.eastmoney.com").
SetHeader("Referer", "https://xuangu.eastmoney.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetBody(fmt.Sprintf(`{
"keyWord": "%s",
"pageSize": 50000,
"pageNo": 1,
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
"gids": [],
"matchWord": "",
"timestamp": "1751113883290349",
"shareToGuba": false,
"requestId": "8xTWgCDAjvQ5lmvz5mDA3Ydk2AE4yoiJ1751113883290",
"needCorrect": true,
"removedConditionIdList": [],
"xcId": "xc0af28549ab330013ed",
"ownSelectAll": false,
"dxInfo": [],
"extraCondition": ""
}`, s.words)).Post(url)
if err != nil {
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
return map[string]any{}
}
respMap := map[string]any{}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap
}

View File

@ -1,25 +0,0 @@
package data
import (
"go-stock/backend/db"
"go-stock/backend/logger"
"testing"
)
func TestSearchStock(t *testing.T) {
db.Init("../../data/stock.db")
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock()
data := res["data"].(map[string]any)
result := data["result"].(map[string]any)
dataList := result["dataList"].([]any)
for _, v := range dataList {
d := v.(map[string]any)
logger.SugaredLogger.Infof("%s:%s", d["INDUSTRY"], d["SECURITY_SHORT_NAME"])
}
//columns := result["columns"].([]any)
//for _, v := range columns {
// logger.SugaredLogger.Infof("v:%+v", v)
//}
}

View File

@ -1,7 +1,6 @@
package data package data
import ( import (
"encoding/json"
"go-stock/backend/db" "go-stock/backend/db"
"go-stock/backend/logger" "go-stock/backend/logger"
"gorm.io/gorm" "gorm.io/gorm"
@ -22,19 +21,6 @@ type Settings struct {
OpenAiModelName string `json:"openAiModelName"` OpenAiModelName string `json:"openAiModelName"`
OpenAiMaxTokens int `json:"openAiMaxTokens"` OpenAiMaxTokens int `json:"openAiMaxTokens"`
OpenAiTemperature float64 `json:"openAiTemperature"` 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"`
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
} }
func (receiver Settings) TableName() string { func (receiver Settings) TableName() string {
@ -68,18 +54,6 @@ func (s SettingsApi) UpdateConfig() string {
"open_ai_max_tokens": s.Config.OpenAiMaxTokens, "open_ai_max_tokens": s.Config.OpenAiMaxTokens,
"open_ai_temperature": s.Config.OpenAiTemperature, "open_ai_temperature": s.Config.OpenAiTemperature,
"tushare_token": s.Config.TushareToken, "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,
"enable_fund": s.Config.EnableFund,
"enable_push_news": s.Config.EnablePushNews,
}) })
} else { } else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config) logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
@ -96,18 +70,6 @@ func (s SettingsApi) UpdateConfig() string {
OpenAiMaxTokens: s.Config.OpenAiMaxTokens, OpenAiMaxTokens: s.Config.OpenAiMaxTokens,
OpenAiTemperature: s.Config.OpenAiTemperature, OpenAiTemperature: s.Config.OpenAiTemperature,
TushareToken: s.Config.TushareToken, 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,
EnableFund: s.Config.EnableFund,
EnablePushNews: s.Config.EnablePushNews,
}) })
} }
return "保存成功!" return "保存成功!"
@ -115,28 +77,5 @@ func (s SettingsApi) UpdateConfig() string {
func (s SettingsApi) GetConfig() *Settings { func (s SettingsApi) GetConfig() *Settings {
var settings Settings var settings Settings
db.Dao.Model(&Settings{}).First(&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()
}
if settings.BrowserPoolSize <= 0 {
settings.BrowserPoolSize = 1
}
return &settings return &settings
} }
func (s SettingsApi) Export() string {
d, _ := json.MarshalIndent(s.GetConfig(), "", " ")
return string(d)
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,15 @@
package data package data
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/PuerkitoBio/goquery"
"github.com/duke-git/lancet/v2/convertor" "github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/random"
"github.com/duke-git/lancet/v2/strutil" "github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"go-stock/backend/db" "go-stock/backend/db"
"go-stock/backend/logger" "go-stock/backend/logger"
"io/ioutil" "io/ioutil"
"regexp"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -23,134 +21,38 @@ import (
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
func TestGetTelegraph(t *testing.T) { func TestGetTelegraph(t *testing.T) {
db.Init("../../data/stock.db") url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
//telegraphs := GetTelegraphList(30) SetHeader("Referer", "https://www.cls.cn/").
//for _, telegraph := range *telegraphs { 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").
// logger.SugaredLogger.Info(telegraph) Get(fmt.Sprintf(url))
//} if err != nil {
list := NewMarketNewsApi().GetNewTelegraph(30) return
for _, telegraph := range *list {
logger.SugaredLogger.Infof("telegraph:%+v", telegraph)
} }
} logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
func TestGetFinancialReports(t *testing.T) { if err != nil {
db.Init("../../data/stock.db") return
//GetFinancialReports("sz000802", 30) }
//GetFinancialReports("hk00927", 30) document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//GetFinancialReports("gb_aapl", 30) text := selection.Text()
GetFinancialReportsByXUEQIU("sz000802", 30) if strings.Contains(text, "【公告】") {
GetFinancialReportsByXUEQIU("gb_aapl", 30) logger.SugaredLogger.Info(text)
GetFinancialReportsByXUEQIU("hk00927", 30) }
})
} }
func TestGetTelegraphSearch(t *testing.T) { func TestGetTelegraphSearch(t *testing.T) {
db.Init("../../data/stock.db")
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram" //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) messages := SearchStockInfo("闻泰科技", "telegram")
for _, message := range *messages { for _, message := range *messages {
logger.SugaredLogger.Info(message) logger.SugaredLogger.Info(message)
} }
//https://www.cls.cn/stock?code=sh600745 //https://www.cls.cn/stock?code=sh600745
} }
func TestSearchStockInfoByCode(t *testing.T) {
db.Init("../../data/stock.db")
SearchStockInfoByCode("sh600745")
}
func TestSearchStockPriceInfo(t *testing.T) { func TestSearchStockPriceInfo(t *testing.T) {
db.Init("../../data/stock.db") SearchStockPriceInfo("sh600745")
//SearchStockPriceInfo("中信证券", "hk06030", 30)
SearchStockPriceInfo("上海贝岭", "sh600171", 30)
//SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
//SearchStockPriceInfo("微创光电", "bj430198", 30)
//getZSInfo("创业板指数", "sz399006", 30)
//getZSInfo("上证综合指数", "sh000001", 30)
//getZSInfo("沪深300指数", "sh000300", 30)
}
func TestGetStockMinutePriceData(t *testing.T) {
db.Init("../../data/stock.db")
data, date := NewStockDataApi().GetStockMinutePriceData("usTSLA.OQ")
logger.SugaredLogger.Infof("date:%s", date)
logger.SugaredLogger.Infof("%+#v", *data)
}
func TestGetKLineData(t *testing.T) {
db.Init("../../data/stock.db")
k := NewStockDataApi().GetKLineData("sh600171", "240", 30)
//for _, kline := range *k {
// logger.SugaredLogger.Infof("%+#v", kline)
//}
jsonData, _ := json.Marshal(*k)
markdownTable, err := JSONToMarkdownTable(jsonData)
if err != nil {
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
}
logger.SugaredLogger.Infof("markdownTable:\n%s", markdownTable)
}
func TestGetHK_KLineData(t *testing.T) {
db.Init("../../data/stock.db")
k := NewStockDataApi().GetHK_KLineData("hk01810", "day", 1)
jsonData, _ := json.Marshal(*k)
markdownTable, err := JSONToMarkdownTable(jsonData)
if err != nil {
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
}
logger.SugaredLogger.Infof("markdownTable:\n%s", markdownTable)
}
func TestGetHKStockInfo(t *testing.T) {
db.Init("../../data/stock.db")
//NewStockDataApi().GetHKStockInfo(200)
//NewStockDataApi().GetSinaHKStockInfo()
//m:105,m:106,m:107 //美股
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
for i := 1; i <= 592; i++ {
NewStockDataApi().getDCStockInfo("us", i, 20)
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
}
}
func TestParseTxStockData(t *testing.T) {
str := "v_r_hk09660=\"100~地平线机器人-W~09660~6.340~5.690~5.800~210980204.0~0~0~6.340~0~0~0~0~0~0~0~0~0~6.340~0~0~0~0~0~0~0~0~0~210980204.0~2025/04/29\n14:14:52~0.650~11.42~6.450~5.710~6.340~210980204.0~1295585259.040~0~33.03~~0~0~13.01~702.2123~836.8986~HORIZONROBOT-W~0.00~10.380~3.320~1.00~-53.74~0~0~0~0~0~33.03~6.50~1.90~600~76.11~19.85~GP~19.70~11.51~0.63~-17.23~46.76~13200293682.00~11075904412.00~33.03~0.000~6.141~58.90~HKD~1~30\";"
//str = "v_sz002241=\"51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~\n~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0\";"
str = "v_sz002241=\"51~歌尔股份~002241~21.92~22.27~22.14~109872~40211~69642~21.91~25~21.90~961~21.89~257~21.88~748~21.87~665~21.92~86~21.93~168~21.94~556~21.95~171~21.96~85~~20250509094209~-0.35~-1.57~22.16~21.84~21.92/109872/241183171~109872~24118~0.36~27.78~~22.16~21.84~1.44~675.97~765.22~2.27~24.50~20.04~2.57~1590~21.95~40.80~28.71~~~1.24~24118.3171~0.0000~0~\n~GP-A~-15.07~5.13~1.11~8.18~3.39~30.63~15.70~5.23~15.67~-25.11~3083811231~3490989083~42.72~10.31~3083811231~~~37.23~0.18~~CNY~0~~21.85~1952\";"
//str = "v_r_hk09660=\"100~地平线机器人-W~09660~6.860~7.000~7.010~21157200.0~0~0~6.860~0~0~0~0~0~0~0~0~0~6.860~0~0~0~0~0~0~0~0~0~21157200.0~2025/05/09\n09:43:13~-0.140~-2.00~7.030~6.730~6.860~21157200.0~144331073.000~0~35.74~~0~0~4.29~759.8070~905.5401~HORIZONROBOT-W~0.00~10.380~3.320~2.93~11.10~0~0~0~0~0~35.74~7.04~0.19~600~90.56~4.73~GP~19.70~11.51~17.26~48.48~13.58~13200293682.00~11075904412.00~35.74~0.000~6.822~71.93~HKD~1~30\";"
info, _ := ParseTxStockData(str)
logger.SugaredLogger.Infof("%+#v", info)
}
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) { func TestParseFullSingleStockData(t *testing.T) {
@ -158,7 +60,7 @@ func TestParseFullSingleStockData(t *testing.T) {
SetHeader("Host", "hq.sinajs.cn"). SetHeader("Host", "hq.sinajs.cn").
SetHeader("Referer", "https://finance.sina.com.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"). 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")) Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600859,sh600745"))
if err != nil { if err != nil {
logger.SugaredLogger.Error(err.Error()) logger.SugaredLogger.Error(err.Error())
} }
@ -166,24 +68,13 @@ func TestParseFullSingleStockData(t *testing.T) {
strs := strutil.SplitEx(data, "\n", true) strs := strutil.SplitEx(data, "\n", true)
for _, str := range strs { for _, str := range strs {
logger.SugaredLogger.Info(str) 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) { func TestNewStockDataApi(t *testing.T) {
db.Init("../../data/stock.db") db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi() stockDataApi := NewStockDataApi()
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla", "hk09660", "hk00700") datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745")
for _, data := range *datas { for _, data := range *datas {
t.Log(data) t.Log(data)
} }
@ -244,7 +135,7 @@ func TestReadFile(t *testing.T) {
func TestFollowedList(t *testing.T) { func TestFollowedList(t *testing.T) {
db.Init("../../data/stock.db") db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi() stockDataApi := NewStockDataApi()
stockDataApi.GetFollowList(1) stockDataApi.GetFollowList()
} }

View File

@ -1,80 +0,0 @@
package data
import (
"go-stock/backend/db"
"gorm.io/gorm"
)
// @Author spark
// @Date 2025/4/3 11:18
// @Desc
// -----------------------------------------------------------------------------------
type Group struct {
gorm.Model
Name string `json:"name" gorm:"index"`
Sort int `json:"sort"`
}
func (Group) TableName() string {
return "stock_groups"
}
type GroupStock struct {
gorm.Model
StockCode string `json:"stockCode" gorm:"index"`
GroupId int `json:"groupId" gorm:"index"`
GroupInfo Group `json:"groupInfo" gorm:"foreignKey:GroupId;references:ID"`
}
func (GroupStock) TableName() string {
return "group_stock_info"
}
type StockGroupApi struct {
dao *gorm.DB
}
func NewStockGroupApi(dao *gorm.DB) *StockGroupApi {
return &StockGroupApi{dao: db.Dao}
}
func (receiver StockGroupApi) AddGroup(group Group) bool {
err := receiver.dao.Where("name = ?", group.Name).FirstOrCreate(&group).Updates(&Group{
Name: group.Name,
Sort: group.Sort,
}).Error
return err == nil
}
func (receiver StockGroupApi) GetGroupList() []Group {
var groups []Group
receiver.dao.Find(&groups)
return groups
}
func (receiver StockGroupApi) GetGroupStockByGroupId(groupId int) []GroupStock {
var stockGroup []GroupStock
receiver.dao.Preload("GroupInfo").Where("group_id = ?", groupId).Find(&stockGroup)
return stockGroup
}
func (receiver StockGroupApi) AddStockGroup(groupId int, stockCode string) bool {
err := receiver.dao.Where("group_id = ? and stock_code = ?", groupId, stockCode).FirstOrCreate(&GroupStock{
GroupId: groupId,
StockCode: stockCode,
}).Updates(&GroupStock{
GroupId: groupId,
StockCode: stockCode,
}).Error
return err == nil
}
func (receiver StockGroupApi) RemoveStockGroup(code string, name string, id int) bool {
err := receiver.dao.Where("group_id = ? and stock_code = ?", id, code).Delete(&GroupStock{}).Error
return err == nil
}
func (receiver StockGroupApi) RemoveGroup(id int) bool {
err := receiver.dao.Where("id = ?", id).Delete(&Group{}).Error
err = receiver.dao.Where("group_id = ?", id).Delete(&GroupStock{}).Error
return err == nil
}

View File

@ -1,290 +0,0 @@
package data
import (
"bufio"
"fmt"
"github.com/go-ego/gse"
"go-stock/backend/logger"
"os"
"strings"
)
// 金融情感词典,包含股票市场相关的专业词汇
var (
seg gse.Segmenter
// 正面金融词汇及其权重
positiveFinanceWords = map[string]float64{
"上涨": 2.0, "涨停": 3.0, "牛市": 3.0, "反弹": 2.0, "新高": 2.5,
"利好": 2.5, "增持": 2.0, "买入": 2.0, "推荐": 1.5, "看多": 2.0,
"盈利": 2.0, "增长": 2.0, "超预期": 2.5, "强劲": 1.5, "回升": 1.5,
"复苏": 2.0, "突破": 2.0, "创新高": 3.0, "回暖": 1.5, "上扬": 1.5,
"利好消息": 3.0, "收益增长": 2.5, "利润增长": 2.5, "业绩优异": 2.5,
"潜力股": 2.0, "绩优股": 2.0, "强势": 1.5, "走高": 1.5, "攀升": 1.5,
"大涨": 2.5, "飙升": 3.0, "井喷": 3.0, "爆发": 2.5, "暴涨": 3.0,
}
// 负面金融词汇及其权重
negativeFinanceWords = map[string]float64{
"下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 1.5, "新低": 2.5,
"利空": 2.5, "减持": 2.0, "卖出": 2.0, "看空": 2.0, "亏损": 2.5,
"下滑": 2.0, "萎缩": 2.0, "不及预期": 2.5, "疲软": 1.5, "恶化": 2.0,
"衰退": 2.0, "跌破": 2.0, "创新低": 3.0, "走弱": 1.5, "下挫": 1.5,
"利空消息": 3.0, "收益下降": 2.5, "利润下滑": 2.5, "业绩不佳": 2.5,
"垃圾股": 2.0, "风险股": 2.0, "弱势": 1.5, "走低": 1.5, "缩量": 2.5,
"大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0,
}
// 否定词,用于反转情感极性
negationWords = map[string]struct{}{
"不": {}, "没": {}, "无": {}, "非": {}, "未": {}, "别": {}, "勿": {},
}
// 程度副词,用于调整情感强度
degreeWords = map[string]float64{
"非常": 1.8, "极其": 2.2, "太": 1.8, "很": 1.5,
"比较": 0.8, "稍微": 0.6, "有点": 0.7, "显著": 1.5,
"大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7,
}
// 转折词,用于识别情感转折
transitionWords = map[string]struct{}{
"但是": {}, "然而": {}, "不过": {}, "却": {}, "可是": {},
}
)
func init() {
// 加载默认词典
err := seg.LoadDict()
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
}
// SentimentResult 情感分析结果类型
type SentimentResult struct {
Score float64 // 情感得分
Category SentimentType // 情感类别
PositiveCount int // 正面词数量
NegativeCount int // 负面词数量
Description string // 情感描述
}
// SentimentType 情感类型枚举
type SentimentType int
const (
Positive SentimentType = iota
Negative
Neutral
)
// AnalyzeSentiment 判断文本的情感
func AnalyzeSentiment(text string) SentimentResult {
// 初始化得分
score := 0.0
positiveCount := 0
negativeCount := 0
// 分词(简单按单个字符分割)
words := splitWords(text)
// 检查文本是否包含转折词,并分割成两部分
var transitionIndex int
var hasTransition bool
for i, word := range words {
if _, ok := transitionWords[word]; ok {
transitionIndex = i
hasTransition = true
break
}
}
// 处理有转折的文本
if hasTransition {
// 转折前的部分
preTransitionWords := words[:transitionIndex]
preScore, prePos, preNeg := calculateScore(preTransitionWords)
// 转折后的部分,权重加倍
postTransitionWords := words[transitionIndex+1:]
postScore, postPos, postNeg := calculateScore(postTransitionWords)
postScore *= 1.5 // 转折后的情感更重要
score = preScore + postScore
positiveCount = prePos + postPos
negativeCount = preNeg + postNeg
} else {
// 没有转折的文本
score, positiveCount, negativeCount = calculateScore(words)
}
// 确定情感类别
var category SentimentType
switch {
case score > 1.0:
category = Positive
case score < -1.0:
category = Negative
default:
category = Neutral
}
return SentimentResult{
Score: score,
Category: category,
PositiveCount: positiveCount,
NegativeCount: negativeCount,
Description: GetSentimentDescription(category),
}
}
// 计算情感得分
func calculateScore(words []string) (float64, int, int) {
score := 0.0
positiveCount := 0
negativeCount := 0
// 遍历每个词,计算情感得分
for i, word := range words {
// 首先检查是否为程度副词
degree, isDegree := degreeWords[word]
// 检查是否为否定词
_, isNegation := negationWords[word]
// 检查是否为金融正面词
if posScore, isPositive := positiveFinanceWords[word]; isPositive {
// 检查前一个词是否为否定词或程度副词
if i > 0 {
prevWord := words[i-1]
if _, isNeg := negationWords[prevWord]; isNeg {
score -= posScore
negativeCount++
continue
}
if deg, isDeg := degreeWords[prevWord]; isDeg {
score += posScore * deg
positiveCount++
continue
}
}
score += posScore
positiveCount++
continue
}
// 检查是否为金融负面词
if negScore, isNegative := negativeFinanceWords[word]; isNegative {
// 检查前一个词是否为否定词或程度副词
if i > 0 {
prevWord := words[i-1]
if _, isNeg := negationWords[prevWord]; isNeg {
score += negScore
positiveCount++
continue
}
if deg, isDeg := degreeWords[prevWord]; isDeg {
score -= negScore * deg
negativeCount++
continue
}
}
score -= negScore
negativeCount++
continue
}
// 处理程度副词(如果后面跟着情感词)
if isDegree && i+1 < len(words) {
nextWord := words[i+1]
if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive {
score += posScore * degree
positiveCount++
continue
}
if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative {
score -= negScore * degree
negativeCount++
continue
}
}
// 处理否定词(如果后面跟着情感词)
if isNegation && i+1 < len(words) {
nextWord := words[i+1]
if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive {
score -= posScore
negativeCount++
continue
}
if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative {
score += negScore
positiveCount++
continue
}
}
}
return score, positiveCount, negativeCount
}
// 简单的分词函数,考虑了中文和英文
func splitWords(text string) []string {
return seg.Cut(text, true)
}
// GetSentimentDescription 获取情感类别的文本描述
func GetSentimentDescription(category SentimentType) string {
switch category {
case Positive:
return "看涨"
case Negative:
return "看跌"
case Neutral:
return "中性"
default:
return "未知"
}
}
func main() {
// 从命令行读取输入
reader := bufio.NewReader(os.Stdin)
fmt.Println("请输入要分析的股市相关文本输入exit退出")
for {
fmt.Print("> ")
text, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取输入时出错:", err)
continue
}
// 去除换行符
text = strings.TrimSpace(text)
// 检查是否退出
if text == "exit" {
break
}
// 分析情感
result := AnalyzeSentiment(text)
// 输出结果
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
GetSentimentDescription(result.Category),
result.Score,
result.PositiveCount,
result.NegativeCount)
}
}

View File

@ -1,36 +0,0 @@
package data
import (
"fmt"
"strings"
"testing"
)
// @Author spark
// @Date 2025/6/19 13:05
// @Desc
//-----------------------------------------------------------------------------------
func TestAnalyzeSentiment(t *testing.T) {
// 分析情感
text := " 【调查韩国近两成中小学生过度使用智能手机或互联网】财联社6月19日电韩国女性家族部18日公布的一项年度调查结果显示接受调查的韩国中小学生中共计约17.3%、即超过21万人使用智能手机或互联网的程度达到了“危险水平”这意味着他们因过度依赖智能手机或互联网而需要关注或干预这一比例引人担忧。 (新华社)\n"
text = "消息人士称联合利华Unilever正在为Graze零食品牌寻找买家。\n"
text = "【韩国未来5年将投入51万亿韩元发展文化产业】 据韩联社韩国文化体育观光部文体部今后5年将投入51万亿韩元约合人民币2667亿元预算落实总统李在明在竞选时期提出的“将韩国打造成全球五大文化强国之一”的承诺。\n"
//text = "【油气股持续拉升 国际实业午后涨停】财联社6月19日电油气股午后持续拉升国际实业、宝莫股份午后涨停准油股份、山东墨龙。茂化实华此前涨停通源石油、海默科技、贝肯能源、中曼石油、科力股份等多股涨超5%。\n"
//text = " 【三大指数均跌逾1% 下跌个股近4800只】财联社6月19日电指数持续走弱沪指下挫跌逾1.00%深成指跌1.25%创业板指跌1.39%。核聚变、风电、军工、食品消费等板块指数跌幅居前沪深京三市下跌个股近4800只。\n"
text = "【银行理财首单网下打新落地】财联社6月20日电记者从多渠道获悉光大理财以申报价格17元参与信通电子网下打新并成功入围有效报价成为行业内首家参与网下打新的银行理财公司。光大理财工作人员向证券时报记者表示本次光大理财是以其管理的混合类产品“阳光橙增盈绝对收益策略”参与了此次网下打新该产品为光大理财“固收+”银行理财产品。资料显示信通电子成立于1996年核心产品包括输电线路智能巡检系统、变电站智能辅控系统、移动智能终端及其他产品。根据其招股说明书信通电子2023、2024年营业收入分别较上年增长19.08%和7.97%净利润分别较上年增长5.6%和15.11%。 (证券时报)"
text = " 【以军称拦截数枚伊朗导弹】财联社6月20日电据央视新闻报道以军在贝尔谢巴及周边区域拦截了数枚伊朗导弹但仍有导弹或拦截残骸落地。以色列国防军发文表示搜救队伍正在一处“空中物体落地”的所在区域开展工作公众目前可以离开避难场所。伊朗方面对上述说法暂无回应。"
words := splitWords(text)
fmt.Println(strings.Join(words, " "))
result := AnalyzeSentiment(text)
// 输出结果
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
result.Description,
result.Score,
result.PositiveCount,
result.NegativeCount)
}

View File

@ -1,93 +0,0 @@
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 ""
}

View File

@ -1,29 +0,0 @@
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)
//
}

File diff suppressed because one or more lines are too long

View File

@ -1,47 +0,0 @@
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)
}

View File

@ -1,364 +0,0 @@
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"`
}
type PromptTemplate struct {
ID int `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `json:"name"`
Content string `json:"content"`
Type string `json:"type"`
}
func (p PromptTemplate) TableName() string {
return "prompt_templates"
}
type Prompt struct {
ID int `json:"ID"`
Name string `json:"name"`
Content string `json:"content"`
Type string `json:"type"`
}
type Telegraph struct {
gorm.Model
Time string `json:"time"`
Content string `json:"content"`
SubjectTags []string `json:"subjects" gorm:"-:all"`
StocksTags []string `json:"stocks" gorm:"-:all"`
IsRed bool `json:"isRed"`
Url string `json:"url"`
Source string `json:"source"`
TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"`
SentimentResult string `json:"sentimentResult" gorm:"-:all"`
}
type TelegraphTags struct {
gorm.Model
TagId uint `json:"tagId"`
TelegraphId uint `json:"telegraphId"`
}
func (t TelegraphTags) TableName() string {
return "telegraph_tags"
}
type Tags struct {
gorm.Model
Name string `json:"name"`
Type string `json:"type"`
}
func (p Tags) TableName() string {
return "tags"
}
func (p Telegraph) TableName() string {
return "telegraph_list"
}
type SinaStockInfo struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
Engname string `json:"engname"`
Tradetype string `json:"tradetype"`
Lasttrade string `json:"lasttrade"`
Prevclose string `json:"prevclose"`
Open string `json:"open"`
High string `json:"high"`
Low string `json:"low"`
Volume string `json:"volume"`
Currentvolume string `json:"currentvolume"`
Amount string `json:"amount"`
Ticktime string `json:"ticktime"`
Buy string `json:"buy"`
Sell string `json:"sell"`
High52Week string `json:"high_52week"`
Low52Week string `json:"low_52week"`
Eps string `json:"eps"`
Dividend string `json:"dividend"`
StocksSum string `json:"stocks_sum"`
Pricechange string `json:"pricechange"`
Changepercent string `json:"changepercent"`
MarketValue string `json:"market_value"`
PeRatio string `json:"pe_ratio"`
}
type LongTigerRankData struct {
ACCUMAMOUNT float64 `json:"ACCUM_AMOUNT"`
BILLBOARDBUYAMT float64 `json:"BILLBOARD_BUY_AMT"`
BILLBOARDDEALAMT float64 `json:"BILLBOARD_DEAL_AMT"`
BILLBOARDNETAMT float64 `json:"BILLBOARD_NET_AMT"`
BILLBOARDSELLAMT float64 `json:"BILLBOARD_SELL_AMT"`
CHANGERATE float64 `json:"CHANGE_RATE"`
CLOSEPRICE float64 `json:"CLOSE_PRICE"`
DEALAMOUNTRATIO float64 `json:"DEAL_AMOUNT_RATIO"`
DEALNETRATIO float64 `json:"DEAL_NET_RATIO"`
EXPLAIN string `json:"EXPLAIN"`
EXPLANATION string `json:"EXPLANATION"`
FREEMARKETCAP float64 `json:"FREE_MARKET_CAP"`
SECUCODE string `json:"SECUCODE" gorm:"index"`
SECURITYCODE string `json:"SECURITY_CODE"`
SECURITYNAMEABBR string `json:"SECURITY_NAME_ABBR"`
SECURITYTYPECODE string `json:"SECURITY_TYPE_CODE"`
TRADEDATE string `json:"TRADE_DATE" gorm:"index"`
TURNOVERRATE float64 `json:"TURNOVERRATE"`
}
func (l LongTigerRankData) TableName() string {
return "long_tiger_rank"
}
type TVNews struct {
Id string `json:"id"`
Title string `json:"title"`
Published int `json:"published"`
Urgency int `json:"urgency"`
Permission string `json:"permission"`
StoryPath string `json:"storyPath"`
Provider struct {
Id string `json:"id"`
Name string `json:"name"`
LogoId string `json:"logo_id"`
} `json:"provider"`
}
type XUEQIUHot struct {
Data struct {
Items []HotItem `json:"items"`
ItemsSize int `json:"items_size"`
} `json:"data"`
ErrorCode int `json:"error_code"`
ErrorDescription string `json:"error_description"`
}
type HotItem struct {
Type int `json:"type"`
Code string `json:"code"`
Name string `json:"name"`
Value float64 `json:"value"`
Increment int `json:"increment"`
RankChange int `json:"rank_change"`
HasExist interface{} `json:"has_exist"`
Symbol string `json:"symbol"`
Percent float64 `json:"percent"`
Current float64 `json:"current"`
Chg float64 `json:"chg"`
Exchange string `json:"exchange"`
StockType int `json:"stock_type"`
SubType string `json:"sub_type"`
Ad int `json:"ad"`
AdId interface{} `json:"ad_id"`
ContentId interface{} `json:"content_id"`
Page interface{} `json:"page"`
Model interface{} `json:"model"`
Location interface{} `json:"location"`
TradeSession interface{} `json:"trade_session"`
CurrentExt interface{} `json:"current_ext"`
PercentExt interface{} `json:"percent_ext"`
}
type HotEvent struct {
PicSize interface{} `json:"pic_size"`
Tag string `json:"tag"`
Id int `json:"id"`
Pic string `json:"pic"`
Hot int `json:"hot"`
StatusCount int `json:"status_count"`
Content string `json:"content"`
}

View File

@ -1,49 +0,0 @@
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)
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>go-stock:AI赋能股票分析</title> <title>wails-naive-demo</title>
<link href="./src/style.css" rel="stylesheet"> <link href="./src/style.css" rel="stylesheet">
</head> </head>
<body> <body>

File diff suppressed because it is too large Load Diff

View File

@ -9,37 +9,17 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@types/file-saver": "^2.0.7", "@vicons/ionicons5": "^0.13.0",
"@vavt/cm-extension": "^1.8.0", "md-editor-v3": "^5.2.1",
"@vavt/v3-extension": "^3.0.0",
"date-fns": "^4.1.0",
"echarts": "^5.6.0",
"file-saver": "^2.0.5",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
"md-editor-v3": "^5.2.3",
"vue": "^3.2.25", "vue": "^3.2.25",
"vue-router": "^4.5.0", "vue-router": "^4.5.0"
"vue3-danmaku": "^1.6.1"
}, },
"devDependencies": { "devDependencies": {
"@vicons/antd": "^0.13.0",
"@vicons/carbon": "^0.13.0",
"@vicons/fa": "^0.13.0",
"@vicons/fluent": "^0.13.0",
"@vicons/ionicons4": "^0.13.0",
"@vicons/ionicons5": "^0.13.0",
"@vicons/material": "^0.13.0",
"@vicons/tabler": "^0.13.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"html-docx-js-typescript": "^0.1.5",
"naive-ui": "^2.41.0", "naive-ui": "^2.41.0",
"vfonts": "^0.0.3", "vfonts": "^0.0.3",
"vite": "^6.3.5" "vite": "5.4.6"
}, },
"keywords": [ "keywords": ["AI赋能股票分析","go-stock"],
"AI赋能股票分析",
"go-stock"
],
"author": "spark" "author": "spark"
} }

View File

@ -1 +1 @@
2d63c3a999d797889c01d6c96451b197 39a415166f03acc0270e24443a9e2445

View File

@ -1,56 +1,29 @@
<script setup> <script setup>
import stockInfo from './components/stock.vue'
import { import {
EventsEmit,
EventsOff,
EventsOn, EventsOn,
Quit, Quit,
WindowFullscreen, WindowFullscreen, WindowGetPosition,
WindowHide, WindowHide,
WindowIsFullscreen, WindowSetPosition,
WindowUnfullscreen WindowUnfullscreen
} from '../wailsjs/runtime' } from '../wailsjs/runtime'
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue"; import {h, ref} from "vue";
import {RouterLink, useRouter} from 'vue-router' import { RouterLink } from 'vue-router'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,dateZhCN,zhCN} from 'naive-ui' import {darkTheme, NIcon, NText,} from 'naive-ui'
import { import {
AlarmOutline, SettingsOutline,
AnalyticsOutline,
BarChartSharp, Bonfire, BonfireOutline, EaselSharp,
ExpandOutline, Flag,
Flame, FlameSharp, InformationOutline,
LogoGithub,
NewspaperOutline,
NewspaperSharp, Notifications,
PowerOutline, Pulse,
ReorderTwoOutline, ReorderTwoOutline,
SettingsOutline, Skull, SkullOutline, SkullSharp, ExpandOutline,
SparklesOutline, RefreshOutline, PowerOutline, BarChartOutline, MoveOutline, WalletOutline, StarOutline,
StarOutline,
Wallet, WarningOutline,
} from '@vicons/ionicons5' } from '@vicons/ionicons5'
import {AnalyzeSentiment, GetConfig, GetGroupList} from "../wailsjs/go/main/App";
import {Dragon, Fire, Gripfire} from "@vicons/fa";
import {ReportSearch} from "@vicons/tabler";
import {LocalFireDepartmentRound} from "@vicons/material";
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
import {FireFilled, FireOutlined, NotificationFilled, StockOutlined} from "@vicons/antd";
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
const router = useRouter()
const loading = ref(true)
const loadingMsg = ref("加载数据中...")
const enableNews = ref(false)
const contentStyle = ref("")
const enableFund = ref(false)
const enableDarkTheme = ref(null)
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎\n\n未经授权,禁止商业目的!')
const isFullscreen = ref(false) const isFullscreen = ref(false)
const activeKey = ref('') const activeKey = ref('stock')
const containerRef = ref({}) const containerRef= ref({})
const realtimeProfit = ref(0) const realtimeProfit= ref(0)
const telegraph = ref([]) const telegraph= ref([])
const groupList = ref([])
const menuOptions = ref([ const menuOptions = ref([
{ {
label: () => label: () =>
@ -59,323 +32,21 @@ const menuOptions = ref([
{ {
to: { to: {
name: 'stock', name: 'stock',
query: { params: {
groupName: '全部', id: 'zh-CN'
groupId: 0,
}, },
params: {},
} }
}, },
{default: () => '股票自选',} { default: () => '我的自选',}
), ),
key: 'stock', key: 'stock',
icon: renderIcon(StarOutline), icon: renderIcon(StarOutline),
children: [ children:[
{ {
label: () => label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '当日盈亏 '+realtimeProfit.value+"¥"}),
h(
'a',
{
href: '#',
type: 'info',
onClick: () => {
//console.log("push",item)
router.push({
name: 'stock',
query: {
groupName: '全部',
groupId: 0,
},
})
EventsEmit("changeTab", {ID: 0, name: '全部'})
},
to: {
name: 'stock',
query: {
groupName: '全部',
groupId: 0,
},
}
},
{default: () => '全部',}
),
key: 0,
}
],
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
params: {}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
{default: () => '市场行情'}
),
key: 'market',
icon: renderIcon(NewspaperOutline),
children: [
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "市场快讯",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
{default: () => '市场快讯',}
),
key: 'market1',
icon: renderIcon(NewspaperSharp),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "全球股指",
},
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
},
},
{default: () => '全球股指',}
),
key: 'market2',
icon: renderIcon(BarChartSharp),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "指标行情",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '指标行情'})
},
},
{default: () => '指标行情',}
),
key: 'market3',
icon: renderIcon(AnalyticsOutline),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "行业排名",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
},
},
{default: () => '行业排名',}
),
key: 'market4',
icon: renderIcon(Flag),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "个股资金流向",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
},
},
{default: () => '个股资金流向',}
),
key: 'market5',
icon: renderIcon(Pulse),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "龙虎榜",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
},
},
{default: () => '龙虎榜',}
),
key: 'market6',
icon: renderIcon(Dragon),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "个股研报",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
},
},
{default: () => '个股研报',}
),
key: 'market7',
icon: renderIcon(StockOutlined),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "公司公告",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
},
},
{default: () => '公司公告',}
),
key: 'market8',
icon: renderIcon(NotificationFilled),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "行业研究",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
},
},
{default: () => '行业研究',}
),
key: 'market9',
icon: renderIcon(ReportSearch),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "当前热门",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
},
},
{default: () => '当前热门',}
),
key: 'market10',
icon: renderIcon(Gripfire),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "指标选股",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
},
},
{default: () => '指标选股',}
),
key: 'market11',
icon: renderIcon(BoxSearch20Regular),
},
]
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'fund',
params: {},
}
},
{default: () => '基金自选',}
),
show: enableFund.value,
key: 'fund',
icon: renderIcon(SparklesOutline),
children: [
{
label: () => h(NText, {type: realtimeProfit.value > 0 ? 'error' : 'success'}, {default: () => '功能完善中!'}),
key: 'realtimeProfit', key: 'realtimeProfit',
show: realtimeProfit.value, show: realtimeProfit.value,
icon: renderIcon(AlarmOutline), icon: renderIcon(WalletOutline),
}, },
] ]
}, },
@ -386,70 +57,55 @@ const menuOptions = ref([
{ {
to: { to: {
name: 'settings', name: 'settings',
params: {} params: {
id: 'zh-CN'
}
} }
}, },
{default: () => '设置'} { default: () => '设置' }
), ),
key: 'settings', key: 'settings',
icon: renderIcon(SettingsOutline), icon: renderIcon(SettingsOutline),
}, },
{ {
label: () => label: ()=> h("a", {
h(
RouterLink,
{
to: {
name: 'about',
params: {}
}
},
{default: () => '关于'}
),
key: 'about',
icon: renderIcon(LogoGithub),
},
{
label: () => h("a", {
href: '#', href: '#',
onClick: toggleFullscreen, onClick: toggleFullscreen,
title: '全屏 Ctrl+F 退出全屏 Esc', title: '全屏 Ctrl+F 退出全屏 Esc',
}, {default: () => isFullscreen.value ? '取消全屏' : '全屏'}), }, { default: () => isFullscreen.value?'取消全屏':'全屏' }),
key: 'full', key: 'full',
icon: renderIcon(ExpandOutline), icon: renderIcon(ExpandOutline),
}, },
{ {
label: () => h("a", { label: ()=> h("a", {
href: '#', href: '#',
onClick: WindowHide, onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+H', title: '隐藏到托盘区 Ctrl+H',
}, {default: () => '隐藏到托盘区'}), }, { default: () => '隐藏到托盘区' }),
key: 'hide', key: 'hide',
icon: renderIcon(ReorderTwoOutline), icon: renderIcon(ReorderTwoOutline),
}, },
// {
// label: ()=> h("a", {
// href: 'javascript:void(0)',
// style: 'cursor: move;',
// onClick: toggleStartMoveWindow,
// }, { default: () => '' }),
// key: 'move',
// icon: renderIcon(MoveOutline),
// },
{ {
label: () => h("a", { label: ()=> h("a", {
href: 'javascript:void(0)',
style: 'cursor: move;',
onClick: toggleStartMoveWindow,
}, { default: () => '移动' }),
key: 'move',
icon: renderIcon(MoveOutline),
},
{
label: ()=> h("a", {
href: '#', href: '#',
onClick: Quit, onClick: Quit,
}, {default: () => '退出程序'}), }, { default: () => '退出程序' }),
key: 'exit', key: 'exit',
icon: renderIcon(PowerOutline), icon: renderIcon(PowerOutline),
}, },
]) ])
function renderIcon(icon) { function renderIcon(icon) {
return () => h(NIcon, null, {default: () => h(icon)}) return () => h(NIcon, null, { default: () => h(icon) })
} }
function toggleFullscreen(e) { function toggleFullscreen(e) {
//console.log(e) //console.log(e)
if (isFullscreen.value) { if (isFullscreen.value) {
@ -459,171 +115,39 @@ function toggleFullscreen(e) {
WindowFullscreen() WindowFullscreen()
// e.target.innerHTML = '' // e.target.innerHTML = ''
} }
isFullscreen.value = !isFullscreen.value 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)
// const drag = ref(false) EventsOn("realtime_profit",(data)=>{
// const lastPos= ref({x:0,y:0}) realtimeProfit.value=data
// 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) => { EventsOn("telegraph",(data)=>{
telegraph.value = data telegraph.value=data
})
EventsOn("loadingMsg", (data) => {
if(data==="done"){
loadingMsg.value = "加载完成..."
EventsEmit("loadingDone", "app")
loading.value = false
}else{
loading.value = true
loadingMsg.value = data
}
})
onBeforeUnmount(() => {
EventsOff("realtime_profit")
EventsOff("loadingMsg")
EventsOff("telegraph")
EventsOff("newsPush")
})
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(() => {
GetGroupList().then(result => {
groupList.value = result
menuOptions.value.map((item) => {
//console.log(item)
if (item.key === 'stock') {
item.children.push(...groupList.value.map(item => {
return {
label: () =>
h(
'a',
{
href: '#',
type: 'info',
onClick: () => {
//console.log("push",item)
router.push({
name: 'stock',
query: {
groupName: item.name,
groupId: item.ID,
},
})
setTimeout(() => {
EventsEmit("changeTab", item)
}, 100)
},
to: {
name: 'stock',
query: {
groupName: item.name,
groupId: item.ID,
},
}
},
{default: () => item.name,}
),
key: item.ID,
}
}))
}
})
})
GetConfig().then((res) => {
//console.log(res)
enableFund.value = res.enableFund
menuOptions.value.filter((item) => {
if (item.key === 'fund') {
item.show = res.enableFund
}
})
if (res.darkTheme) {
enableDarkTheme.value = darkTheme
} else {
enableDarkTheme.value = null
}
})
})
onMounted(() => {
contentStyle.value = "max-height: calc(92vh);overflow: hidden"
GetConfig().then((res) => {
if (res.enableNews) {
enableNews.value = true
}
enableFund.value = res.enableFund
const {notification } =createDiscreteApi(["notification"], {
configProviderProps: {
theme: enableDarkTheme.value ? darkTheme : lightTheme ,
max: 3,
},
})
EventsOn("newsPush", (data) => {
//console.log(data)
if(data.isRed){
notification.create({
//type:"error",
// avatar: () => h(NIcon,{component:Notifications,color:"red"}),
title: data.time,
content: () => h(NText,{type:"error"}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*40,
})
}else{
notification.create({
//type:"info",
//avatar: () => h(NIcon,{component:Notifications}),
title: data.time,
content: () => h(NText,{type:"info"}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*30 ,
})
}
})
})
}) })
</script> </script>
<template> <template>
<n-config-provider ref="containerRef" :theme="enableDarkTheme" :locale="zhCN" :date-locale="dateZhCN">
<n-message-provider>
<n-config-provider :theme="darkTheme" ref="containerRef">
<n-message-provider >
<n-notification-provider> <n-notification-provider>
<n-modal-provider> <n-modal-provider>
<n-dialog-provider>
<n-watermark <n-watermark
:content="content" :content="content"
cross cross
@ -636,27 +160,21 @@ onMounted(() => {
:y-offset="150" :y-offset="150"
:rotate="-15" :rotate="-15"
> >
<n-flex> <n-flex justify="center">
<n-grid x-gap="12" :cols="1"> <n-grid x-gap="12" :cols="1">
<n-gi> <n-gi style="position: relative;top:1px;z-index: 19;width: 100%" v-if="telegraph.length>0">
<n-spin :show="loading"> <n-marquee :speed="120" >
<template #description>
{{ loadingMsg }}
</template>
<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"> <n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
{{ item }} {{item}}
</n-tag> </n-tag>
<!-- <n-text type="warning"> {{telegraph[0]}}</n-text>-->
</n-marquee> </n-marquee>
<n-scrollbar :style="contentStyle">
<n-skeleton v-if="loading" height="calc(100vh)" />
<RouterView/>
</n-scrollbar>
</n-spin>
</n-gi> </n-gi>
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;"> <n-gi style="padding-bottom: 70px;padding-top: 5px">
<n-card size="small" style="--wails-draggable:drag"> <RouterView />
</n-gi>
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%">
<n-card size="small">
<n-menu style="font-size: 18px;" <n-menu style="font-size: 18px;"
v-model:value="activeKey" v-model:value="activeKey"
mode="horizontal" mode="horizontal"
@ -666,9 +184,9 @@ onMounted(() => {
</n-card> </n-card>
</n-gi> </n-gi>
</n-grid> </n-grid>
</n-flex> </n-flex>
</n-watermark> </n-watermark>
</n-dialog-provider>
</n-modal-provider> </n-modal-provider>
</n-notification-provider> </n-notification-provider>
</n-message-provider> </n-message-provider>

View File

@ -1,102 +0,0 @@
<script setup lang="ts">
import {nextTick, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {ClsCalendar} from "../../wailsjs/go/main/App";
import { addMonths, format ,parse} from 'date-fns';
import { zhCN } from 'date-fns/locale';
import {useMessage} from 'naive-ui'
import {Star48Filled} from "@vicons/fluent";
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0'); // 0+1
const day = String(today.getDate()).padStart(2, '0');
// YYYY-MM-DD
const formattedDate = `${year}-${month}-${day}`;
const formattedYM = `${year}-${month}`;
const list = ref([])
const message=useMessage()
function goBackToday() {
setTimeout(() => {
nextTick(
() => {
const elementById = document.getElementById(formattedDate);
if (elementById) {
elementById.scrollIntoView({
behavior: 'auto',
block: 'start'
})
}
}
)
}, 500)
}
onBeforeMount(() => {
ClsCalendar().then(res => {
list.value = res
goBackToday();
})
})
function getweekday(date){
let day=parse(date, 'yyyy-MM-dd', new Date())
return format(day, 'EEEE', {locale: zhCN})
}
</script>
<template>
<!-- <n-timeline size="large" style="text-align: left">-->
<!-- <n-timeline-item v-for="item in list" :key="item.date" :title="item.date" type="info" >-->
<!-- <n-list>-->
<!-- <n-list-item v-for="l in item.list" :key="l.article_id ">-->
<!-- <n-text>{{l.title}}</n-text>-->
<!-- </n-list-item>-->
<!-- </n-list>-->
<!-- </n-timeline-item>-->
<!-- </n-timeline>-->
<n-list bordered style="max-height: calc(100vh - 230px);text-align: left;">
<n-scrollbar style="max-height: calc(100vh - 230px);" >
<n-list-item v-for="(item, index) in list" :id="item.calendar_day" :key="item.calendar_day">
<n-thing :title="item.calendar_day +' '+item.week">
<n-list :bordered="false" hoverable>
<n-list-item v-for="(l,i ) in item.items" :key="l.id ">
<n-flex justify="space-between">
<n-text :type="item.calendar_day===formattedDate?'warning':'info'">{{i+1}}# {{l.title}}
<n-tag v-if="l.event" size="small" round type="success">事件</n-tag>
<n-tag v-if="l.economic" size="small" round type="error">数据</n-tag>
</n-text>
<n-rate v-if="l.event&&(l.event.star>0)" readonly :default-value="l.event.star">
<n-icon :component="Star48Filled"/>
</n-rate>
<n-rate v-if="l.economic&&(l.economic.star>0)" readonly :default-value="l.economic.star" >
<n-icon :component="Star48Filled"/>
</n-rate>
</n-flex>
<n-flex v-if="l.economic">
<n-tag type="warning" :bordered="false" :size="'small'">公布{{l.economic.actual }}</n-tag>
<n-tag type="warning" :bordered="false" :size="'small'">预测{{l.economic.consensus}}</n-tag>
<n-tag type="warning" :bordered="false" :size="'small'">前值{{l.economic.front}}</n-tag>
</n-flex>
</n-list-item>
</n-list>
</n-thing>
</n-list-item>
<n-list-item v-if="list.length==0">
<n-text type="info">没有数据</n-text>
</n-list-item>
<n-list-item v-else style="text-align: center;">
<n-button-group>
<n-button strong secondary type="warning" @click="goBackToday">回到今天</n-button>
</n-button-group>
</n-list-item>
</n-scrollbar>
</n-list>
</template>
<style scoped>
</style>

View File

@ -1,37 +0,0 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotEvent} from "../../wailsjs/go/main/App";
const list = ref([])
const task =ref()
onBeforeMount(async () => {
list.value = await HotEvent(50)
task.value=setInterval(async ()=>{
list.value = await HotEvent(50)
}, 1000*10)
})
onUnmounted(async ()=>{
clearInterval(task.value)
})
</script>
<template>
<n-list bordered>
<template #header>
雪球热门
</template>
<n-list-item v-for="(item, index) in list" :key="index">
<n-thing :title="item.tag" :description="item.content" >
<template v-if="item.pic" #avatar>
<n-avatar :src="item.pic" :size="60">
</n-avatar>
</template>
</n-thing>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@ -1,88 +0,0 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotStock} from "../../wailsjs/go/main/App";
import KLineChart from "./KLineChart.vue";
import {ArrowBack, ArrowDown, ArrowUp} from "@vicons/ionicons5";
const {marketType}=defineProps(
{
marketType: {
type: String,
default: '10'
}
}
)
const task =ref()
const list = ref([])
onBeforeMount(async () => {
list.value = await HotStock(marketType)
task.value = setInterval(async () => {
list.value = await HotStock(marketType)
}, 5000)
})
onUnmounted(()=>{
clearInterval(task.value)
})
function getMarketCode(item) {
if (item.exchange === 'SZ') {
return item.code.toLowerCase()
}
if (item.exchange === 'SH') {
return item.code.toLowerCase()
}
if (item.exchange === 'HK') {
return (item.exchange + item.code).toLowerCase()
}
return ("gb_"+item.code).toLowerCase()
}
</script>
<template>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>股票名称</n-th>
<n-th>涨跌幅</n-th>
<n-th>当前价格</n-th>
<n-th>热度</n-th>
<n-th>热度变化</n-th>
<n-th>排名变化</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.code">
<n-td><n-text type="info">
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-tag type="info" :bordered="false"> {{item.name}} {{item.code}}</n-tag>
</template>
<k-line-chart style="width: 800px" :code="getMarketCode(item)" :chart-height="500" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-text></n-td>
<n-td><n-text :type="item.percent>0?'error':'success'">{{item.percent}}%</n-text></n-td>
<n-td><n-text type="info">{{item.current}}</n-text></n-td>
<n-td><n-text type="info">{{item.value}}</n-text></n-td>
<n-td><n-text :type="item.increment>0?'error':'success'">
{{item.increment}}
<n-icon v-if="item.increment>0" :component="ArrowUp"/>
<n-icon v-else :component="ArrowDown"/>
</n-text></n-td>
<n-td>
<n-text :type="item.rank_change>0?'error':'success'">
{{item.rank_change}}
<n-icon v-if="item.rank_change>0" :component="ArrowUp"/>
<n-text v-else-if="item.rank_change==0" ></n-text>
<n-icon v-else :component="ArrowDown"/>
</n-text>
</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -1,73 +0,0 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotTopic} from "../../wailsjs/go/main/App";
const list = ref([])
const task =ref()
onBeforeMount(async () => {
list.value = await HotTopic(10)
setInterval(async ()=>{
list.value = await HotTopic(10)
}, 1000*10)
})
onUnmounted(()=>{
clearInterval(task.value)
})
function openCenteredWindow(url, width, height) {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
return window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top}`
);
}
function showPage(htid) {
openCenteredWindow(`https://gubatopic.eastmoney.com/topic_v3.html?htid=${htid}`, 1000, 600)
}
</script>
<template>
<n-list bordered hoverable clickable>
<!-- <template #header>-->
<!-- 股吧热门-->
<!-- </template>-->
<n-list-item v-for="(item, index) in list" :key="index">
<n-thing :title="item.nickname" :description="item.desc" :description-style="'font-size: 14px;'" @click="showPage(item.htid)">
<template v-if="item.squareImg" #avatar>
<n-avatar :src="item.squareImg" :size="60">
</n-avatar>
</template>
<template v-if="item.stock_list" #footer>
<n-flex>
<n-tag type="info" v-for="(v, i) in item.stock_list" :bordered="false" size="small">
{{v.name}}
</n-tag>
</n-flex>
</template>
<template v-if="item.clickNumber" #header-extra>
<n-flex>
<n-button secondary type="warning" size="tiny">讨论数<n-number-animation
show-separator
:from="0"
:to="item.postNumber"
/>
</n-button >
<n-tag :bordered="false" type="warning" size="small">浏览量<n-number-animation
show-separator
:from="0"
:to="item.clickNumber"
/>
</n-tag>
</n-flex>
</template>
</n-thing>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@ -1,115 +0,0 @@
<script setup>
import {onBeforeMount, ref} from 'vue'
import {GetStockList, IndustryResearchReport,EMDictCode} from "../../wailsjs/go/main/App";
import {ArrowDownOutline, CaretDown, CaretUp, PulseOutline, Refresh, RefreshCircleSharp,} from "@vicons/ionicons5";
import {useMessage} from "naive-ui";
import {BrowserOpenURL} from "../../wailsjs/runtime";
const message=useMessage()
const list = ref([])
const options = ref([])
function getIndustryResearchReport(value) {
message.loading("正在刷新数据...")
IndustryResearchReport(value).then(result => {
console.log(result)
list.value = result
})
}
onBeforeMount(()=>{
getIndustryResearchReport('');
})
function ratingChangeName(ratingChange){
if(ratingChange===0){
return '调高'
}else if(ratingChange===1){
return '调低'
}else if(ratingChange===2){
return '首次'
}else if(ratingChange===3){
return '维持'
}else if (ratingChange===4){
return '无变化'
}else{
return ''
}
}
function openWin(code) {
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H3_"+code+"_1.pdf?1749744888000.pdf")
}
function EMDictCodeList(keyVal){
if (keyVal){
EMDictCode('016').then(result => {
console.log(result)
options.value=result.filter((value,index,array) => value.bkName.includes(keyVal)||value.firstLetter.includes(keyVal)||value.bkCode.includes(keyVal)).map(item => {
return {
label: item.bkName+" - "+item.bkCode,
value: item.bkCode
}
})
})
}else{
getIndustryResearchReport('')
}
}
function handleSearch(value) {
getIndustryResearchReport(value)
}
</script>
<template>
<n-card>
<n-auto-complete :options="options" placeholder="请输入行业名称关键词搜索" clearable filterable :on-select="handleSearch" :on-update:value="EMDictCodeList" />
</n-card>
<n-table striped size="small">
<n-thead>
<n-tr>
<!-- <n-th>代码</n-th>-->
<!-- <n-th>名称</n-th>-->
<n-th>行业</n-th>
<n-th>标题</n-th>
<n-th>东财评级</n-th>
<n-th>评级变动</n-th>
<n-th>机构评级</n-th>
<n-th>分析师</n-th>
<n-th>机构</n-th>
<n-th> <n-flex justify="space-between">日期<n-icon @click="getIndustryResearchReport" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.infoCode">
<!-- <n-td>{{item.stockCode}}</n-td>-->
<!-- <n-td :title="item.stockCode">-->
<!-- <n-popover trigger="hover" placement="right">-->
<!-- <template #trigger>-->
<!-- <n-tag type="info" :bordered="false">{{item.stockName}}</n-tag>-->
<!-- </template>-->
<!-- <k-line-chart style="width: 800px" :code="getmMarketCode(item.market,item.stockCode)" :chart-height="500" :name="item.stockName" :k-days="20" :dark-theme="true"></k-line-chart>-->
<!-- </n-popover>-->
<!-- </n-td>-->
<n-td><n-tag type="info" :bordered="false">{{item.industryName}}</n-tag></n-td>
<n-td>
<n-a type="info" @click="openWin(item.infoCode)"><n-text type="success">{{item.title}}</n-text></n-a>
</n-td>
<n-td><n-text :type="item.emRatingName==='增持'?'error':'info'">
{{item.emRatingName}}
</n-text></n-td>
<n-td><n-text :type="item.ratingChange===0?'error':'info'">{{ratingChangeName(item.ratingChange)}}</n-text></n-td>
<n-td>{{item.sRatingName }}</n-td>
<n-td><n-ellipsis style="max-width: 120px">{{item.researcher}}</n-ellipsis></n-td>
<n-td>{{item.orgSName}}</n-td>
<n-td>{{item.publishDate.substring(0,10)}}</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -1,108 +0,0 @@
<script setup lang="ts">
import {nextTick, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {InvestCalendarTimeLine} from "../../wailsjs/go/main/App";
import { addMonths, format ,parse} from 'date-fns';
import { zhCN } from 'date-fns/locale';
import {useMessage} from 'naive-ui'
import {Star48Filled} from "@vicons/fluent";
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0'); // 0+1
const day = String(today.getDate()).padStart(2, '0');
// YYYY-MM-DD
const formattedDate = `${year}-${month}-${day}`;
const formattedYM = `${year}-${month}`;
const list = ref([])
const message=useMessage()
function goBackToday() {
setTimeout(() => {
nextTick(
() => {
const elementById = document.getElementById(formattedDate);
if (elementById) {
elementById.scrollIntoView({
behavior: 'auto',
block: 'start'
})
}
}
)
}, 500)
}
onBeforeMount(() => {
InvestCalendarTimeLine(formattedYM).then(res => {
list.value = res
goBackToday();
})
})
onMounted(()=>{
})
function loadMore(){
if (list.value.length>0){
let day=parse(list.value[list.value.length-1].date, 'yyyy-MM-dd', new Date())
let nextMonth=addMonths(day,1)
let ym = format(nextMonth, 'yyyy-MM');
console.log(ym)
InvestCalendarTimeLine(ym).then(res => {
if (res.length==0){
message.warning("没有更多数据了")
return
}
list.value.push( ...res)
})
}
}
function getweekday(date){
let day=parse(date, 'yyyy-MM-dd', new Date())
return format(day, 'EEEE', {locale: zhCN})
}
</script>
<template>
<!-- <n-timeline size="large" style="text-align: left">-->
<!-- <n-timeline-item v-for="item in list" :key="item.date" :title="item.date" type="info" >-->
<!-- <n-list>-->
<!-- <n-list-item v-for="l in item.list" :key="l.article_id ">-->
<!-- <n-text>{{l.title}}</n-text>-->
<!-- </n-list-item>-->
<!-- </n-list>-->
<!-- </n-timeline-item>-->
<!-- </n-timeline>-->
<n-list bordered style="max-height: calc(100vh - 230px);text-align: left;">
<n-scrollbar style="max-height: calc(100vh - 230px);" >
<n-list-item v-for="(item, index) in list" :id="item.date" :key="item.date">
<n-thing :title="item.date+' '+getweekday(item.date)">
<n-list :bordered="false" hoverable>
<n-list-item v-for="(l,i ) in item.list" :key="l.article_id ">
<n-flex justify="space-between">
<n-text :type="item.date===formattedDate?'warning':'info'">{{i+1}}# {{l.title}}</n-text>
<n-rate v-if="l.like_count>0" readonly :default-value="l.like_count" :count="l.like_count" >
<n-icon :component="Star48Filled"/>
</n-rate>
</n-flex>
</n-list-item>
</n-list>
</n-thing>
</n-list-item>
<n-list-item v-if="list.length==0">
<n-text type="info">没有数据</n-text>
</n-list-item>
<n-list-item v-else style="text-align: center;">
<n-button-group>
<n-button strong secondary type="info" @click="loadMore">加载更多</n-button>
<n-button strong secondary type="warning" @click="goBackToday">回到今天</n-button>
</n-button-group>
</n-list-item>
</n-scrollbar>
</n-list>
</template>
<style scoped>
</style>

View File

@ -1,386 +0,0 @@
<script setup>
import {GetStockKLine} from "../../wailsjs/go/main/App";
import * as echarts from "echarts";
import {onMounted, ref} from "vue";
import _ from "lodash";
const { code,name,darkTheme,kDays ,chartHeight} = defineProps({
code: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
kDays: {
type: Number,
default: 14
},
chartHeight: {
type: Number,
default: 500
},
darkTheme: {
type: Boolean,
default: false
}
})
const upColor = '#ec0000';
const upBorderColor = '';
const downColor = '#00da3c';
const downBorderColor = '';
const kLineChartRef = ref(null);
onMounted(() => {
handleKLine(code,name)
})
function handleKLine(code,name){
GetStockKLine(code,name,365).then(result => {
//console.log("GetStockKLine",result)
const chart = echarts.init(kLineChartRef.value);
const categoryData = [];
const values = [];
const volumns=[];
for (let i = 0; i < result.length; i++) {
let resultElement=result[i]
//console.log("resultElement:{}",resultElement)
categoryData.push(resultElement.day)
let flag=resultElement.close>resultElement.open?1:-1
values.push([
resultElement.open,
resultElement.close,
resultElement.low,
resultElement.high
])
volumns.push([i,resultElement.volume/10000,flag])
}
////console.log("categoryData",categoryData)
////console.log("values",values)
let option = {
title: {
text: name+" "+code,
left: '20px',
textStyle: {
color: darkTheme?'#ccc':'#456'
}
},
darkMode: darkTheme,
//backgroundColor: '#1c1c1c',
// color:['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],
animation: false,
legend: {
right: 20,
top: 0,
data: ['日K', 'MA5', 'MA10', 'MA20', 'MA30'],
textStyle: {
color: darkTheme?'#ccc':'#456'
},
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
lineStyle: {
color: '#376df4',
width: 2,
opacity: 1
}
},
borderWidth: 2,
borderColor: darkTheme?'#456':'#ccc',
backgroundColor: darkTheme?'#456':'#fff',
padding: 10,
textStyle: {
color: darkTheme?'#ccc':'#456'
},
formatter: function (params) {//
//console.log("params",params)
let currentItemData = _.filter(params, (param) => param.seriesIndex === 0)[0].data;
let ma5=_.filter(params, (param) => param.seriesIndex === 1)[0].data;//ma5
let ma10=_.filter(params, (param) => param.seriesIndex === 2)[0].data;//ma10
let ma20=_.filter(params, (param) => param.seriesIndex === 3)[0].data;//ma20
let ma30=_.filter(params, (param) => param.seriesIndex === 4)[0].data;//ma30
let volum=_.filter(params, (param) => param.seriesIndex === 5)[0].data;
return _.filter(params, (param) => param.seriesIndex === 0)[0].name + '<br>' +
'开盘:' + currentItemData[1] + '<br>' +
'收盘:' + currentItemData[2] + '<br>' +
'最低:' + currentItemData[3] + '<br>' +
'最高:' + currentItemData[4] + '<br>' +
'成交量(万手):' + volum[1] + '<br>' +
'MA5日均线:' + ma5 + '<br>' +
'MA10日均线:' + ma10 + '<br>' +
'MA20日均线:' + ma20 + '<br>' +
'MA30日均线:' + ma30
}
},
axisPointer: {
link: [
{
xAxisIndex: 'all'
}
],
label: {
backgroundColor: '#888'
}
},
visualMap: {
show: false,
seriesIndex: 5,
dimension: 2,
pieces: [
{
value: -1,
color: downColor
},
{
value: 1,
color: upColor
}
]
},
grid: [
{
left: '8%',
right: '8%',
height: '50%',
},
{
left: '8%',
right: '8%',
top: '66%',
height: '15%'
}
],
xAxis: [
{
type: 'category',
data: categoryData,
boundaryGap: false,
axisLine: { onZero: false },
splitLine: { show: false },
min: 'dataMin',
max: 'dataMax',
axisPointer: {
z: 100
}
},
{
type: 'category',
gridIndex: 1,
data: categoryData,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
min: 'dataMin',
max: 'dataMax'
}
],
yAxis: [
{
scale: true,
splitArea: {
show: true
}
},
{
scale: true,
gridIndex: 1,
splitNumber: 2,
axisLabel: { show: false },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false }
}
],
dataZoom: [
{
type: 'inside',
xAxisIndex: [0, 1],
start: 100-kDays,
end: 100
},
{
show: true,
xAxisIndex: [0, 1],
type: 'slider',
top: '85%',
start: 100-kDays,
end: 100
}
],
series: [
{
name: '日K',
type: 'candlestick',
data: values,
itemStyle: {
color: upColor,
color0: downColor,
// borderColor: upBorderColor,
// borderColor0: downBorderColor
},
markPoint: {
//symbol: 'none',
label: {
formatter: function (param) {
return param != null ? param.value + '' : '';
}
},
data: [
{
name: '最高',
type: 'max',
valueDim: 'highest'
},
{
name: '最低',
type: 'min',
valueDim: 'lowest'
},
{
name: '平均收盘价',
type: 'average',
valueDim: 'close'
}
],
tooltip: {
formatter: function (param) {
return param.name + '<br>' + (param.data.coord || '');
}
}
},
markLine: {
symbol: ['none', 'none'],
data: [
[
{
name: 'from lowest to highest',
type: 'min',
valueDim: 'lowest',
symbol: 'circle',
symbolSize: 10,
label: {
show: false
},
emphasis: {
label: {
show: false
}
}
},
{
type: 'max',
valueDim: 'highest',
symbol: 'circle',
symbolSize: 10,
label: {
show: false
},
emphasis: {
label: {
show: false
}
}
}
],
{
name: 'min line on close',
type: 'min',
valueDim: 'close'
},
{
name: 'max line on close',
type: 'max',
valueDim: 'close'
}
]
}
},
{
name: 'MA5',
type: 'line',
data: calculateMA(5,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: 'MA10',
type: 'line',
data: calculateMA(10,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: 'MA20',
type: 'line',
data: calculateMA(20,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: 'MA30',
type: 'line',
data: calculateMA(30,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: '成交量(手)',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: '#7fbe9e'
},
data: volumns
}
]
};
chart.setOption(option);
chart.on('click',{seriesName:'日K'}, function(params) {
//console.log("click:",params);
});
})
}
function calculateMA(dayCount,values) {
var result = [];
for (var i = 0, len = values.length; i < len; i++) {
if (i < dayCount) {
result.push('-');
continue;
}
var sum = 0;
for (var j = 0; j < dayCount; j++) {
sum += +values[i - j][1];
}
result.push((sum / dayCount).toFixed(2));
}
return result;
}
</script>
<template>
<div ref="kLineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
</template>
<style scoped>
</style>

View File

@ -1,229 +0,0 @@
<script setup lang="ts">
import {onBeforeMount, ref} from 'vue'
import {LongTigerRank} from "../../wailsjs/go/main/App";
import {BrowserOpenURL} from "../../wailsjs/runtime";
import {ArrowDownOutline} from "@vicons/ionicons5";
import _ from "lodash";
import KLineChart from "./KLineChart.vue";
import MoneyTrend from "./moneyTrend.vue";
import {NButton, NText, useMessage} from "naive-ui";
const message = useMessage()
const lhbList= ref([])
const EXPLANATIONs = ref([])
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0'); // 0+1
const day = String(today.getDate()).padStart(2, '0');
// YYYY-MM-DD
const formattedDate = `${year}-${month}-${day}`;
const SearchForm= ref({
dateValue: formattedDate,
EXPLANATION:null,
})
onBeforeMount(() => {
longTiger(formattedDate);
})
function longTiger_old(date) {
if(date) {
SearchForm.value.dateValue = date
}
let loading1=message.loading("正在获取龙虎榜数据...",{
duration: 0,
})
LongTigerRank(date).then(res => {
lhbList.value = res
loading1.destroy()
if (res.length === 0) {
message.info("暂无数据,请切换日期")
}
EXPLANATIONs.value=_.uniqBy(_.map(lhbList.value,function (item){
return {
label: item['EXPLANATION'],
value: item['EXPLANATION'],
}
}),'label');
})
}
function longTiger(date) {
if (date) {
SearchForm.value.dateValue = date;
}
let loading1 = message.loading("正在获取龙虎榜数据...", {
duration: 0,
});
const fetchDate = (currentDate, retryCount = 0) => {
if (retryCount > 7) { // 7
lhbList.value = [];
EXPLANATIONs.value = [];
loading1.destroy();
message.info("暂无历史数据");
return;
}
LongTigerRank(currentDate).then(res => {
if (res.length === 0) {
const previousDate = new Date(currentDate);
previousDate.setDate(previousDate.getDate() - 1);
const year = previousDate.getFullYear();
const month = String(previousDate.getMonth() + 1).padStart(2, '0');
const day = String(previousDate.getDate()).padStart(2, '0');
const prevFormattedDate = `${year}-${month}-${day}`;
message.info(`当前日期 ${currentDate} 暂无数据,尝试查询前一日:${prevFormattedDate}`);
SearchForm.value.dateValue = prevFormattedDate;
fetchDate(prevFormattedDate, retryCount + 1); //
} else {
lhbList.value = res;
loading1.destroy();
EXPLANATIONs.value = _.uniqBy(_.map(lhbList.value, function (item) {
return {
label: item['EXPLANATION'],
value: item['EXPLANATION'],
};
}), 'label');
}
}).catch(err => {
loading1.destroy();
message.error("获取数据失败,请重试");
console.error(err);
});
};
fetchDate(date || formattedDate);
}
function handleEXPLANATION(value, option){
SearchForm.value.EXPLANATION = value
if(value){
LongTigerRank(SearchForm.value.dateValue).then(res => {
lhbList.value=_.filter(res, function(o) { return o['EXPLANATION']===value; });
if (res.length === 0) {
message.info("暂无数据,请切换日期")
}
})
}else{
longTiger(SearchForm.value.dateValue)
}
}
</script>
<template>
<n-form :model="SearchForm" >
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="4" label="日期" path="dateValue" label-placement="left">
<n-date-picker v-model:formatted-value="SearchForm.dateValue"
value-format="yyyy-MM-dd" type="date" :on-update:value="(v,v2)=>longTiger(v2)"/>
</n-form-item-gi>
<n-form-item-gi :span="8" label="上榜原因" path="EXPLANATION" label-placement="left">
<n-select clearable placeholder="上榜原因过滤" v-model:value="SearchForm.EXPLANATION" :options="EXPLANATIONs" :on-update:value="handleEXPLANATION"/>
</n-form-item-gi>
<n-form-item-gi :span="10" label="" label-placement="left">
<n-text type="error">*当天的龙虎榜数据通常在收盘结束后一小时左右更新</n-text>
</n-form-item-gi>
</n-grid>
</n-form>
<n-table :single-line="false" striped>
<n-thead>
<n-tr>
<n-th>代码</n-th>
<!-- <n-th width="90px">日期</n-th>-->
<n-th width="60px">名称</n-th>
<n-th>收盘价</n-th>
<n-th width="60px">涨跌幅</n-th>
<n-th>龙虎榜净买额()</n-th>
<n-th>龙虎榜买入额()</n-th>
<n-th>龙虎榜卖出额()</n-th>
<n-th>龙虎榜成交额()</n-th>
<!-- <n-th>市场总成交额()</n-th>-->
<!-- <n-th>净买额占总成交比</n-th>-->
<!-- <n-th>成交额占总成交比</n-th>-->
<n-th width="60px" data-field="TURNOVERRATE">换手率<n-icon :component="ArrowDownOutline" /></n-th>
<n-th>流通市值(亿)</n-th>
<n-th>上榜原因</n-th>
<!-- <n-th>解读</n-th>-->
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="(item, index) in lhbList" :key="index">
<n-td>
<n-tag :bordered=false type="info">{{ item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0] }}</n-tag>
</n-td>
<!-- <n-td>
{{item.TRADE_DATE.substring(0,10)}}
</n-td>-->
<n-td>
<!-- <n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.SECURITY_NAME_ABBR }}</n-text>-->
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-button tag="a" text :type="item.CHANGE_RATE>0?'error':'success'" :bordered=false >{{ item.SECURITY_NAME_ABBR }}</n-button>
</template>
<k-line-chart style="width: 800px" :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :chart-height="500" :name="item.SECURITY_NAME_ABBR" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td>
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.CLOSE_PRICE }}</n-text>
</n-td>
<n-td>
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ (item.CHANGE_RATE).toFixed(2) }}%</n-text>
</n-td>
<n-td>
<!-- <n-text :type="item.BILLBOARD_NET_AMT>0?'error':'success'">{{ (item.BILLBOARD_NET_AMT/10000).toFixed(2) }}</n-text>-->
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-button tag="a" text :type="item.BILLBOARD_NET_AMT>0?'error':'success'" :bordered=false >{{ (item.BILLBOARD_NET_AMT/10000).toFixed(2) }}</n-button>
</template>
<money-trend :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :name="item.SECURITY_NAME_ABBR" :days="360" :dark-theme="true" :chart-height="500" style="width: 800px"></money-trend>
</n-popover>
</n-td>
<n-td>
<n-text :type="'error'">{{ (item.BILLBOARD_BUY_AMT/10000).toFixed(2) }}</n-text>
</n-td>
<n-td>
<n-text :type="'success'">{{ (item.BILLBOARD_SELL_AMT/10000).toFixed(2) }}</n-text>
</n-td>
<n-td>
<n-text :type="'info'">{{ (item.BILLBOARD_DEAL_AMT/10000).toFixed(2) }}</n-text>
</n-td>
<!-- <n-td>-->
<!-- <n-text :type="'info'">{{ (item.ACCUM_AMOUNT/10000).toFixed(2) }}</n-text>-->
<!-- </n-td>-->
<!-- <n-td>-->
<!-- <n-text :type="item.DEAL_NET_RATIO>0?'error':'success'">{{ (item.DEAL_NET_RATIO).toFixed(2) }}%</n-text>-->
<!-- </n-td>-->
<!-- <n-td>-->
<!-- <n-text :type="'info'">{{ (item.DEAL_AMOUNT_RATIO).toFixed(2) }}%</n-text>-->
<!-- </n-td>-->
<n-td>
<n-text :type="'info'">{{ (item.TURNOVERRATE).toFixed(2) }}%</n-text>
</n-td>
<n-td>
<n-text :type="'info'">{{ (item.FREE_MARKET_CAP/100000000).toFixed(2) }}</n-text>
</n-td>
<n-td>
<n-text :type="'info'">{{ item.EXPLANATION }}</n-text>
</n-td>
<!-- <n-td>
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.EXPLAIN }}</n-text>
</n-td>-->
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -1,126 +0,0 @@
<script setup lang="ts">
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {SearchStock} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag} from 'naive-ui'
const message = useMessage()
const search = ref('科技股;换手率连续3日大于2')
const columns = ref([])
const dataList = ref([])
function Search() {
const loading = message.loading("正在获取选股数据...", {duration: 0});
SearchStock(search.value).then(res => {
loading.destroy()
//console.log(res)
if(res.code==100){
message.success(res.msg)
columns.value=res.data.result.columns.filter(item=>!item.hiddenNeed&&(item.title!="市场码"&&item.title!="市场简称")).map(item=>{
if(item.children){
return {
title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key,
resizable: true,
minWidth:200,
ellipsis: {
tooltip: true
},
children:item.children.filter(item=>!item.hiddenNeed).map(item=>{
return {
title:item.dateMsg,
key:item.key,
minWidth:100,
resizable: true,
ellipsis: {
tooltip: true
}
}
})
}
}else{
return {
title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key,
resizable: true,
minWidth:100,
ellipsis: {
tooltip: true
}
}
}
})
dataList.value=res.data.result.dataList
}else {
message.error(res.msg)
}
}).catch(err => {
message.error(err)
})
}
function isNumeric(value) {
return !isNaN(parseFloat(value)) && isFinite(value);
}
onBeforeMount(() => {
Search()
})
</script>
<template>
<n-flex>
<n-input-group>
<n-input v-model:value="search" placeholder="请输入选股指标或者要求" />
<n-button type="success" @click="Search">搜索A股</n-button>
</n-input-group>
</n-flex>
<!-- <n-table striped size="small">-->
<!-- <n-thead>-->
<!-- <n-tr>-->
<!-- <n-th v-for="item in columns">{{item.title}}</n-th>-->
<!-- </n-tr>-->
<!-- </n-thead>-->
<!-- <n-tbody>-->
<!-- <n-tr v-for="(item,index) in dataList">-->
<!-- <n-td v-for="d in columns">{{item[d.key]}}</n-td>-->
<!-- </n-tr>-->
<!-- </n-tbody>-->
<!-- </n-table>-->
<n-data-table
:max-height="'calc(100vh - 285px)'"
size="small"
:columns="columns"
:data="dataList"
:pagination="false"
:scroll-x="1800"
:render-cell="(value, rowData, column) => {
if(column.key=='SECURITY_CODE'||column.key=='SERIAL'){
return h(NText, { type: 'info',border: false }, { default: () => `${value}` })
}
if (isNumeric(value)) {
let type='info';
if (Number(value)<0){
type='success';
}
if(Number(value)>=0&&Number(value)<=5){
type='warning';
}
if (Number(value)>5){
type='error';
}
return h(NText, { type: type }, { default: () => `${value}` })
}else{
if(column.key=='SECURITY_SHORT_NAME'){
return h(NTag, { type: 'info',bordered: false }, { default: () => `${value}` })
}else{
return h(NText, { type: 'info' }, { default: () => `${value}` })
}
}
}"
/>
</template>
<style scoped>
</style>

View File

@ -1,148 +0,0 @@
<script setup lang="ts">
import {onBeforeMount, ref} from 'vue'
import {GetStockList, StockNotice} from "../../wailsjs/go/main/App";
import {BrowserOpenURL} from "../../wailsjs/runtime";
import {RefreshCircleSharp} from "@vicons/ionicons5";
import _ from "lodash";
import KLineChart from "./KLineChart.vue";
import MoneyTrend from "./moneyTrend.vue";
import {useMessage} from "naive-ui";
const {stockCode}=defineProps(
{
stockCode: {
type: String,
default: ''
}
}
)
const list = ref([])
const options = ref([])
const message=useMessage()
function getNotice(stockCodes) {
StockNotice(stockCodes).then(result => {
console.log(result)
list.value = result
})
}
onBeforeMount (()=>{
//message.info(""+stockCode)
getNotice(stockCode);
})
function findStockList(query){
if (query){
GetStockList(query).then(result => {
options.value=result.map(item => {
return {
label: item.name+" - "+item.ts_code,
value: item.ts_code
}
})
})
}else{
getNotice("")
}
}
function handleSearch(value) {
getNotice(value)
}
function openWin(code) {
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H2_"+code+"_1.pdf?1750092081000.pdf")
}
function getTypeColor(name){
if(name.includes("质押")||name.includes("冻结")||name.includes("解冻")||name.includes("解押")||name.includes("解禁")){
return "error"
}
if(name.includes("异常")||name.includes("减持")||name.includes("增发")||name.includes("重大")){
return "error"
}
if(name.includes("季度报告")||name.includes("年度报告")||name.includes("澄清公告")||name.includes("风险")){
return "error"
}
if(name.includes("终止")||name.includes("复牌")||name.includes("停牌")||name.includes("退市")){
return "error"
}
if(name.includes("破产")||name.includes("清算")){
return "error"
}
if(name.includes("回购")||name.includes("重组")||name.includes("诉讼")||name.includes("仲裁")||name.includes("转让")||name.includes("收购")){
return "warning"
}
if(name.includes("调研")||name.includes("募集")){
return "warning"
}
return "info"
}
function getmMarketCode(market,code) {
if(market==="0"){
return "sz"+code
}else if(market==="1"){
return "sh"+code
}else if(market==="2"){
return "bj"+code
}else if(market==="3"){
return "hk"+code
}else{
return code
}
}
</script>
<template>
<n-card>
<n-auto-complete :options="options" placeholder="请输入A股名称或者代码" clearable filterable :on-select="handleSearch" :on-update:value="findStockList" />
</n-card>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>股票代码</n-th>
<n-th>股票名称</n-th>
<n-th>公告标题</n-th>
<n-th>公告类型</n-th>
<n-th>公告日期</n-th>
<n-th><n-flex>数据更新时间<n-icon @click="getNotice('')" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.art_code">
<n-td>
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-tag type="info" :bordered="false">{{item.codes[0].stock_code }}</n-tag>
</template>
<money-trend style="width: 800px" :code="getmMarketCode(item.codes[0].market_code,item.codes[0].stock_code)" :name="item.codes[0].short_name" :days="360" :dark-theme="true" :chart-height="500"></money-trend>
</n-popover>
</n-td>
<n-td>
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-tag type="info" :bordered="false">{{item.codes[0].short_name }}</n-tag>
</template>
<k-line-chart style="width: 800px" :code="getmMarketCode(item.codes[0].market_code,item.codes[0].stock_code)" :chart-height="500" :name="item.codes[0].short_name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td>
<n-a type="info" @click="openWin(item.art_code)"><n-text :type="getTypeColor(item.columns[0].column_name)"> {{item.title}}</n-text></n-a>
</n-td>
<n-td>
<n-text :type="getTypeColor(item.columns[0].column_name)">{{item.columns[0].column_name }}</n-text>
</n-td>
<n-td>
<n-tag type="info">{{item.notice_date.substring(0,10) }}</n-tag>
</n-td>
<n-td>
<n-tag type="info">{{item.display_time.substring(0,19)}}</n-tag>
</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -1,136 +0,0 @@
<script setup>
import {onBeforeMount, ref} from 'vue'
import {GetStockList, StockResearchReport} from "../../wailsjs/go/main/App";
import {ArrowDownOutline, CaretDown, CaretUp, PulseOutline, Refresh, RefreshCircleSharp,} from "@vicons/ionicons5";
import KLineChart from "./KLineChart.vue";
import MoneyTrend from "./moneyTrend.vue";
import {useMessage} from "naive-ui";
import {BrowserOpenURL} from "../../wailsjs/runtime";
const {stockCode}=defineProps(
{
stockCode: {
type: String,
default: ''
}
}
)
const message=useMessage()
const list = ref([])
const options = ref([])
function getStockResearchReport(value) {
StockResearchReport(value).then(result => {
//console.log(result)
list.value = result
})
}
onBeforeMount(()=>{
getStockResearchReport(stockCode);
})
function ratingChangeName(ratingChange){
if(ratingChange===0){
return '调高'
}else if(ratingChange===1){
return '调低'
}else if(ratingChange===2){
return '首次'
}else if(ratingChange===3){
return '维持'
}else if (ratingChange===4){
return '无变化'
}else{
return ''
}
}
function getmMarketCode(market,code) {
if(market==="SHENZHEN"){
return "sz"+code
}else if(market==="SHANGHAI"){
return "sh"+code
}else if(market==="BEIJING"){
return "bj"+code
}else if(market==="HONGKONG"){
return "hk"+code
}else{
return code
}
}
function openWin(code) {
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H3_"+code+"_1.pdf?1749744888000.pdf")
}
function findStockList(query){
if (query){
GetStockList(query).then(result => {
options.value=result.map(item => {
return {
label: item.name+" - "+item.ts_code,
value: item.ts_code
}
})
})
}else{
getStockResearchReport('')
}
}
function handleSearch(value) {
getStockResearchReport(value)
}
</script>
<template>
<n-card>
<n-auto-complete :options="options" placeholder="请输入A股名称或者代码" clearable filterable :on-select="handleSearch" :on-update:value="findStockList" />
</n-card>
<n-table striped size="small">
<n-thead>
<n-tr>
<!-- <n-th>代码</n-th>-->
<n-th>名称</n-th>
<n-th>行业</n-th>
<n-th>标题</n-th>
<n-th>东财评级</n-th>
<n-th>评级变动</n-th>
<n-th>机构评级</n-th>
<n-th>分析师</n-th>
<n-th>机构</n-th>
<n-th> <n-flex justify="space-between">日期<n-icon @click="getStockResearchReport" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.infoCode">
<!-- <n-td>{{item.stockCode}}</n-td>-->
<n-td :title="item.stockCode">
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-tag type="info" :bordered="false">{{item.stockName}}</n-tag>
</template>
<k-line-chart style="width: 800px" :code="getmMarketCode(item.market,item.stockCode)" :chart-height="500" :name="item.stockName" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td><n-tag type="info" :bordered="false">{{item.indvInduName}}</n-tag></n-td>
<n-td>
<n-a type="info" @click="openWin(item.infoCode)">{{item.title}}</n-a>
</n-td>
<n-td><n-text :type="item.emRatingName==='增持'?'error':'info'">
{{item.emRatingName}}
</n-text></n-td>
<n-td><n-text :type="item.ratingChange===0?'error':'info'">{{ratingChangeName(item.ratingChange)}}</n-text></n-td>
<n-td>{{item.sRatingName}}</n-td>
<n-td>{{item.researcher}}</n-td>
<n-td>{{item.orgSName}}</n-td>
<n-td>{{item.publishDate.substring(0,10)}}</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -1,220 +0,0 @@
<script setup>
// import { MdPreview } from 'md-editor-v3';
// preview.cssstyle.css
import 'md-editor-v3/lib/preview.css';
import {h, onBeforeUnmount, onMounted, ref} from 'vue';
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
import {EventsOff, 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;
});
})
onBeforeUnmount(() => {
notify.destroyAll()
EventsOff("updateVersion")
})
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>支持DeepSeekOpenAI OllamaLMStudioAnythingLLM<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/CodeNoobLH" target="_blank">浓睡不消残酒</a><n-divider vertical />
<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>
本软件基于开源技术构建使用WailsNaiveUIVue等开源项目技术上如有问题可以先向对应的开源社区请求帮助
</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>
QQ506808970微信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>

View File

@ -1,278 +0,0 @@
<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()
message.destroyAll()
})
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>&nbsp;
<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}}&nbsp;
{{info.netEstimatedRate}} %&nbsp;&nbsp;&nbsp;
({{info.netEstimatedUnitTime}})</n-text>
<br>
<n-text size="small" :type="info.netEstimatedRate>0?'error':'success'" :bordered="false" v-if="info.netUnitValue">
单位净值{{info.netUnitValue}}&nbsp;&nbsp;&nbsp; ({{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>

View File

@ -1,94 +0,0 @@
<script setup>
import {CaretDown, CaretUp, RefreshCircleOutline} from "@vicons/ionicons5";
import {NText,useMessage} from "naive-ui";
import {onBeforeUnmount, onMounted, onUnmounted, ref} from "vue";
import {GetIndustryMoneyRankSina} from "../../wailsjs/go/main/App";
import KLineChart from "./KLineChart.vue";
const props = defineProps({
headerTitle: {
type: String,
default: '行业资金排名(净流入)'
},
fenlei: {
type: String,
default: '0'
},
sort: {
type: String,
default: 'netamount'
},
})
const message = useMessage()
const dataList= ref([])
const sort = ref(props.sort)
const fenlei= ref(props.fenlei)
const interval = ref(null)
onMounted(()=>{
sort.value=props.sort
fenlei.value=props.fenlei
GetRankData()
interval.value=setInterval(()=>{
GetRankData()
},1000*60)
})
onBeforeUnmount(()=>{
clearInterval(interval.value)
})
function GetRankData(){
message.loading("正在刷新数据...")
GetIndustryMoneyRankSina(fenlei.value,sort.value).then(result => {
if(result.length>0){
dataList.value = result
//console.log(result)
}
})
}
</script>
<template>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>板块名称</n-th>
<n-th>涨跌幅</n-th>
<n-th>流入资金/</n-th>
<n-th>流出资金/</n-th>
<n-th>净流入/<n-icon v-if="sort==='0'" :component="CaretDown"/><n-icon v-if="sort==='1'" :component="CaretUp"/></n-th>
<n-th>净流入率</n-th>
<n-th>领涨股</n-th>
<n-th>涨跌幅</n-th>
<n-th>最新价</n-th>
<n-th>净流入率</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in dataList" :key="item.category">
<n-td><n-tag :bordered=false type="info">{{item.name}}</n-tag></n-td>
<n-td> <n-text :type="item.avg_changeratio>0?'error':'success'">{{(item.avg_changeratio*100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text type="info">{{(item.inamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info">{{(item.outamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text :type="item.netamount>0?'error':'success'">{{(item.netamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text :type="item.ratioamount>0?'error':'success'">{{(item.ratioamount*100).toFixed(2)}}%</n-text></n-td>
<n-td>
<!-- <n-text type="info">{{item.ts_name}}</n-text>-->
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-button tag="a" text :type="item.ts_changeratio>0?'error':'success'" :bordered=false >{{ item.ts_name }}</n-button>
</template>
<k-line-chart style="width: 800px" :code="item.ts_symbol" :chart-height="500" :name="item.ts_name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td><n-text :type="item.ts_changeratio>0?'error':'success'">{{(item.ts_changeratio*100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text type="info">{{item.ts_trade}}</n-text></n-td>
<n-td><n-text :type="item.ts_ratioamount>0?'error':'success'">{{(item.ts_ratioamount*100).toFixed(2)}}%</n-text></n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -1,655 +0,0 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, ref} from 'vue'
import {
GetAIResponseResult,
GetConfig,
GetIndustryRank,
GetPromptTemplates,
GetTelegraphList,
GlobalStockIndexes,
ReFleshTelegraphList,
SaveAIResponseResult,
SaveAsMarkdown,
ShareAnalysis,
SummaryStockNews
} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import NewsList from "./newsList.vue";
import KLineChart from "./KLineChart.vue";
import { CaretDown, CaretUp, PulseOutline,} from "@vicons/ionicons5";
import {NAvatar, NButton, NFlex, NText, useMessage, useNotification} from "naive-ui";
import {MdPreview} from "md-editor-v3";
import {useRoute} from 'vue-router'
import RankTable from "./rankTable.vue";
import IndustryMoneyRank from "./industryMoneyRank.vue";
import StockResearchReportList from "./StockResearchReportList.vue";
import StockNoticeList from "./StockNoticeList.vue";
import LongTigerRankList from "./LongTigerRankList.vue";
import IndustryResearchReportList from "./IndustryResearchReportList.vue";
import HotStockList from "./HotStockList.vue";
import HotEvents from "./HotEvents.vue";
import HotTopics from "./HotTopics.vue";
import InvestCalendarTimeLine from "./InvestCalendarTimeLine.vue";
import ClsCalendarTimeLine from "./ClsCalendarTimeLine.vue";
import SelectStock from "./SelectStock.vue";
const route = useRoute()
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
const message = useMessage()
const notify = useNotification()
const panelHeight = ref(window.innerHeight - 240)
const telegraphList = ref([])
const sinaNewsList = ref([])
const common = ref([])
const america = ref([])
const europe = ref([])
const asia = ref([])
const other = ref([])
const globalStockIndexes = ref(null)
const summaryModal = ref(false)
const summaryBTN = ref(true)
const darkTheme = ref(false)
const theme = computed(() => {
return darkTheme ? 'dark' : 'light'
})
const aiSummary = ref(``)
const aiSummaryTime = ref("")
const modelName = ref("")
const chatId = ref("")
const question = ref(``)
const sysPromptId = ref(0)
const loading = ref(true)
const sysPromptOptions = ref([])
const userPromptOptions = ref([])
const promptTemplates = ref([])
const industryRanks = ref([])
const sort = ref("0")
const nowTab = ref("市场快讯")
const indexInterval = ref(null)
const indexIndustryRank = ref(null)
const stockCode= ref('')
function getIndex() {
GlobalStockIndexes().then((res) => {
globalStockIndexes.value = res
common.value = res["common"]
america.value = res["america"]
europe.value = res["europe"]
asia.value = res["asia"]
other.value = res["other"]
})
}
onBeforeMount(() => {
nowTab.value = route.query.name
stockCode.value = route.query.stockCode
GetConfig().then(result => {
summaryBTN.value = result.openAiEnable
darkTheme.value = result.darkTheme
})
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
sysPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型系统Prompt')
userPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型用户Prompt')
})
GetTelegraphList("财联社电报").then((res) => {
telegraphList.value = res
})
GetTelegraphList("新浪财经").then((res) => {
sinaNewsList.value = res
})
getIndex();
industryRank();
indexInterval.value = setInterval(() => {
getIndex()
}, 3000)
indexIndustryRank.value = setInterval(() => {
industryRank()
}, 1000 * 10)
})
onBeforeUnmount(() => {
EventsOff("changeMarketTab")
EventsOff("newTelegraph")
EventsOff("newSinaNews")
EventsOff("summaryStockNews")
clearInterval(indexInterval.value)
clearInterval(indexIndustryRank.value)
})
EventsOn("changeMarketTab", async (msg) => {
//message.info(msg.name)
updateTab(msg.name)
})
EventsOn("newTelegraph", (data) => {
for (let i = 0; i < data.length; i++) {
telegraphList.value.pop()
}
telegraphList.value.unshift(...data)
})
EventsOn("newSinaNews", (data) => {
for (let i = 0; i < data.length; i++) {
sinaNewsList.value.pop()
}
sinaNewsList.value.unshift(...data)
})
//
window.onresize = () => {
panelHeight.value = window.innerHeight - 240
}
function getAreaName(code) {
switch (code) {
case "america":
return "美洲"
case "europe":
return "欧洲"
case "asia":
return "亚洲"
case "common":
return "常用"
case "other":
return "其他"
}
}
function changeIndustryRankSort() {
if (sort.value === "0") {
sort.value = "1"
} else {
sort.value = "0"
}
industryRank()
}
function industryRank() {
GetIndustryRank(sort.value, 150).then(result => {
if (result.length > 0) {
//console.log(result)
industryRanks.value = result
} else {
message.info("暂无数据")
}
})
}
function reAiSummary() {
aiSummary.value = ""
summaryModal.value = true
loading.value = true
SummaryStockNews(question.value, sysPromptId.value)
}
function getAiSummary() {
summaryModal.value = true
loading.value = true
GetAIResponseResult("市场资讯").then(result => {
if (result.content) {
aiSummary.value = result.content
question.value = result.question
loading.value = false
const date = new Date(result.CreatedAt);
const year = date.getFullYear();
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');
aiSummaryTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
modelName.value = result.modelName
} else {
aiSummaryTime.value = ""
aiSummary.value = ""
modelName.value = ""
SummaryStockNews(question.value, sysPromptId.value)
}
})
}
function updateTab(name) {
summaryBTN.value = (name === "市场快讯");
nowTab.value = name
}
EventsOn("summaryStockNews", async (msg) => {
loading.value = false
////console.log(msg)
if (msg === "DONE") {
SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value)
message.info("AI分析完成")
message.destroyAll()
} else {
if (msg.chatId) {
chatId.value = msg.chatId
}
if (msg.question) {
question.value = msg.question
}
if (msg.content) {
aiSummary.value = aiSummary.value + msg.content
}
if (msg.extraContent) {
aiSummary.value = aiSummary.value + msg.extraContent
}
if (msg.model) {
modelName.value = msg.model
}
if (msg.time) {
aiSummaryTime.value = msg.time
}
}
})
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(aiSummary.value);
message.success('分析结果已复制到剪切板');
} catch (err) {
message.error('复制失败: ' + err);
}
}
function saveAsMarkdown() {
SaveAsMarkdown('市场资讯', '市场资讯').then(result => {
message.success(result)
})
}
function share() {
ShareAnalysis('市场资讯', '市场资讯').then(msg => {
//message.info(msg)
notify.info({
avatar: () =>
h(NAvatar, {
size: 'small',
round: false,
src: icon.value
}),
title: '分享到社区',
duration: 1000 * 30,
content: () => {
return h('div', {
style: {
'text-align': 'left',
'font-size': '14px',
}
}, {default: () => msg})
},
})
})
}
function ReFlesh(source) {
//console.log("ReFlesh:", source)
ReFleshTelegraphList(source).then(res => {
if (source === "财联社电报") {
telegraphList.value = res
}
if (source === "新浪财经") {
sinaNewsList.value = res
}
})
}
</script>
<template>
<n-card>
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab">
<n-tab-pane name="市场快讯" tab="市场快讯">
<n-grid :cols="2" :y-gap="0">
<n-gi>
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
</n-gi>
<n-gi>
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
</n-gi>
</n-grid>
</n-tab-pane>
<n-tab-pane name="全球股指" tab="全球股指">
<n-tabs type="segment" animated>
<n-tab-pane name="全球指数" tab="全球指数">
<n-grid :cols="5" :y-gap="0">
<n-gi v-for="(val, key) in globalStockIndexes" :key="key">
<n-list bordered>
<template #header>
{{ getAreaName(key) }}
</template>
<n-list-item v-for="item in val" :key="item.code">
<n-grid :cols="3" :y-gap="0">
<n-gi>
<n-text :type="item.zdf>0?'error':'success'">
<n-image :src="item.img" width="20"/> &nbsp;{{ item.name }}
</n-text>
</n-gi>
<n-gi>
<n-text :type="item.zdf>0?'error':'success'">{{ item.zxj }}</n-text>&nbsp;
<n-text :type="item.zdf>0?'error':'success'">
<n-number-animation :precision="2" :from="0" :to="item.zdf"/>
%
</n-text>
</n-gi>
<n-gi>
<n-text :type="item.state === 'open' ? 'success' : 'warning'">{{
item.state === 'open' ? '开市' : '休市'
}}
</n-text>
</n-gi>
</n-grid>
</n-list-item>
</n-list>
</n-gi>
</n-grid>
</n-tab-pane>
<n-tab-pane name="上证指数" tab="上证指数">
<k-line-chart code="sh000001" :chart-height="panelHeight" name="上证指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="深证成指" tab="深证成指">
<k-line-chart code="sz399001" :chart-height="panelHeight" name="深证成指" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="创业板指" tab="创业板指">
<k-line-chart code="sz399006" :chart-height="panelHeight" name="创业板指" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="恒生指数" tab="恒生指数">
<k-line-chart code="hkHSI" :chart-height="panelHeight" name="恒生指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="纳斯达克" tab="纳斯达克">
<k-line-chart code="us.IXIC" :chart-height="panelHeight" name="纳斯达克" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="道琼斯" tab="道琼斯">
<k-line-chart code="us.DJI" :chart-height="panelHeight" name="道琼斯" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="标普500" tab="标普500">
<k-line-chart code="us.INX" :chart-height="panelHeight" name="标普500" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="指标行情" tab="指标行情">
<n-tabs type="segment" animated>
<n-tab-pane name="科创50" tab="科创50">
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="沪深300" tab="沪深300">
<k-line-chart code="sh000300" :chart-height="panelHeight" name="沪深300" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="上证50" tab="上证50">
<k-line-chart code="sh000016" :chart-height="panelHeight" name="上证50" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证A500" tab="中证A500">
<k-line-chart code="sh000510" :chart-height="panelHeight" name="中证A500" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证1000" tab="中证1000">
<k-line-chart code="sh000852" :chart-height="panelHeight" name="中证1000" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证白酒" tab="中证白酒">
<k-line-chart code="sz399997" :chart-height="panelHeight" name="中证白酒" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="富时中国三倍做多" tab="富时中国三倍做多">
<k-line-chart code="usYINN.AM" :chart-height="panelHeight" name="富时中国三倍做多" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="VIX恐慌指数" tab="VIX恐慌指数">
<k-line-chart code="usUVXY.AM" :chart-height="panelHeight" name="VIX恐慌指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="行业排名" tab="行业排名">
<n-tabs type="card" animated>
<n-tab-pane name="行业涨幅排名" tab="行业涨幅排名">
<n-table striped>
<n-thead>
<n-tr>
<n-th>行业名称</n-th>
<n-th @click="changeIndustryRankSort">行业涨幅
<n-icon v-if="sort==='0'" :component="CaretDown"/>
<n-icon v-if="sort==='1'" :component="CaretUp"/>
</n-th>
<n-th>行业5日涨幅</n-th>
<n-th>行业20日涨幅</n-th>
<n-th>领涨股</n-th>
<n-th>涨幅</n-th>
<n-th>最新价</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in industryRanks" :key="item.bd_code">
<n-td>
<n-tag :bordered=false type="info">{{ item.bd_name }}</n-tag>
</n-td>
<n-td>
<n-text :type="item.bd_zdf>0?'error':'success'">{{ item.bd_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf5>0?'error':'success'">{{ item.bd_zdf5 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf20>0?'error':'success'">{{ item.bd_zdf20 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_name }}
<n-text type="info">{{ item.nzg_code }}</n-text>
</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'">{{ item.nzg_zxj }}</n-text>
</n-td>
</n-tr>
</n-tbody>
</n-table>
<n-table striped>
<n-thead>
<n-tr>
<n-th>行业名称</n-th>
<n-th @click="changeIndustryRankSort">行业涨幅
<n-icon v-if="sort==='0'" :component="CaretDown"/>
<n-icon v-if="sort==='1'" :component="CaretUp"/>
</n-th>
<n-th>行业5日涨幅</n-th>
<n-th>行业20日涨幅</n-th>
<n-th>领涨股</n-th>
<n-th>涨幅</n-th>
<n-th>最新价</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in industryRanks" :key="item.bd_code">
<n-td>
<n-tag :bordered=false type="info">{{ item.bd_name }}</n-tag>
</n-td>
<n-td>
<n-text :type="item.bd_zdf>0?'error':'success'">{{ item.bd_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf5>0?'error':'success'">{{ item.bd_zdf5 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf20>0?'error':'success'">{{ item.bd_zdf20 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_name }}
<n-text type="info">{{ item.nzg_code }}</n-text>
</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'">{{ item.nzg_zxj }}</n-text>
</n-td>
</n-tr>
</n-tbody>
</n-table>
</n-tab-pane>
<n-tab-pane name="行业资金排名(净流入)" tab="行业资金排名">
<industryMoneyRank :fenlei="'0'" :header-title="'行业资金排名(净流入)'" :sort="'netamount'"/>
</n-tab-pane>
<n-tab-pane name="证监会行业资金排名(净流入)" tab="证监会行业资金排名">
<industryMoneyRank :fenlei="'2'" :header-title="'证监会行业资金排名(净流入)'" :sort="'netamount'"/>
</n-tab-pane>
<n-tab-pane name="概念板块资金排名(净流入)" tab="概念板块资金排名">
<industryMoneyRank :fenlei="'1'" :header-title="'概念板块资金排名(净流入)'" :sort="'netamount'"/>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="个股资金流向" tab="个股资金流向">
<n-tabs type="card" animated>
<n-tab-pane name="netamount" tab="净流入额排名">
<RankTable :header-title="'净流入额排名'" :sort="'netamount'"/>
</n-tab-pane>
<n-tab-pane name="outamount" tab="流出资金排名">
<RankTable :header-title="'流出资金排名'" :sort="'outamount'"/>
</n-tab-pane>
<n-tab-pane name="ratioamount" tab="净流入率排名">
<RankTable :header-title="'净流入率排名'" :sort="'ratioamount'"/>
</n-tab-pane>
<n-tab-pane name="r0_net" tab="主力净流入额排名">
<RankTable :header-title="'主力净流入额排名'" :sort="'r0_net'"/>
</n-tab-pane>
<n-tab-pane name="r0_out" tab="主力流出排名">
<RankTable :header-title="'主力流出排名'" :sort="'r0_out'"/>
</n-tab-pane>
<n-tab-pane name="r0_ratio" tab="主力净流入率排名">
<RankTable :header-title="'主力净流入率排名'" :sort="'r0_ratio'"/>
</n-tab-pane>
<n-tab-pane name="r3_net" tab="散户净流入额排名">
<RankTable :header-title="'散户净流入额排名'" :sort="'r3_net'"/>
</n-tab-pane>
<n-tab-pane name="r3_out" tab="散户流出排名">
<RankTable :header-title="'散户流出排名'" :sort="'r3_out'"/>
</n-tab-pane>
<n-tab-pane name="r3_ratio" tab="散户净流入率排名">
<RankTable :header-title="'散户净流入率排名'" :sort="'r3_ratio'"/>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="龙虎榜" tab="龙虎榜">
<LongTigerRankList />
</n-tab-pane>
<n-tab-pane name="个股研报" tab="个股研报">
<StockResearchReportList :stock-code="stockCode"/>
</n-tab-pane>
<n-tab-pane name="公司公告" tab="公司公告 ">
<StockNoticeList :stock-code="stockCode" />
</n-tab-pane>
<n-tab-pane name="行业研究" tab="行业研究 ">
<IndustryResearchReportList/>
</n-tab-pane>
<n-tab-pane name="当前热门" tab="当前热门">
<n-tabs type="card" animated>
<n-tab-pane name="全球" tab="全球">
<HotStockList :market-type="'10'"/>
</n-tab-pane>
<n-tab-pane name="沪深" tab="沪深">
<HotStockList :market-type="'12'"/>
</n-tab-pane>
<n-tab-pane name="港股" tab="港股">
<HotStockList :market-type="'13'"/>
</n-tab-pane>
<n-tab-pane name="美股" tab="美股">
<HotStockList :market-type="'11'"/>
</n-tab-pane>
<n-tab-pane name="热门话题" tab="热门话题">
<n-grid :cols="1" :y-gap="10">
<n-grid-item>
<HotTopics/>
</n-grid-item>
<!-- <n-grid-item>-->
<!-- <HotEvents/>-->
<!-- </n-grid-item>-->
</n-grid>
</n-tab-pane>
<n-tab-pane name="重大事件时间轴" tab="重大事件时间轴">
<InvestCalendarTimeLine />
</n-tab-pane>
<n-tab-pane name="财经日历" tab="财经日历">
<ClsCalendarTimeLine />
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="指标选股" tab="指标选股">
<select-stock />
</n-tab-pane>
</n-tabs>
</n-card>
<n-modal transform-origin="center" v-model:show="summaryModal" preset="card" style="width: 800px;"
:title="'AI市场资讯总结'">
<n-spin size="small" :show="loading">
<MdPreview style="height: 440px;text-align: left" :modelValue="aiSummary" :theme="theme"/>
</n-spin>
<template #footer>
<n-flex justify="space-between" ref="tipsRef">
<n-text type="info" v-if="aiSummaryTime">
<n-tag v-if="modelName" type="warning" round :title="chatId" :bordered="false">{{ modelName }}</n-tag>
{{ aiSummaryTime }}
</n-text>
<n-text type="error">*AI分析结果仅供参考请以实际行情为准投资需谨慎风险自担</n-text>
</n-flex>
</template>
<template #action>
<n-flex justify="space-between" style="margin-bottom: 10px">
<n-select style="width: 49%" v-model:value="sysPromptId" label-field="name" value-field="ID"
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
<n-select style="width: 49%" v-model:value="question" label-field="name" value-field="content"
:options="userPromptOptions" placeholder="请选择用户提示词"/>
</n-flex>
<n-flex justify="right">
<n-input v-model:value="question" style="text-align: left" clearable
type="textarea"
:show-count="true"
placeholder="请输入您的问题:例如 总结和分析股票市场新闻中的投资机会"
:autosize="{
minRows: 2,
maxRows: 5
}"
/>
<n-button size="tiny" type="warning" @click="reAiSummary">再次总结</n-button>
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
<n-button size="tiny" type="primary" @click="saveAsMarkdown">保存为Markdown文件</n-button>
<n-button size="tiny" type="error" @click="share">分享到项目社区</n-button>
</n-flex>
</template>
</n-modal>
<div style="position: fixed;bottom: 18px;right:25px;z-index: 10;" v-if="summaryBTN">
<n-input-group>
<n-button type="primary" @click="getAiSummary">
<n-icon :component="PulseOutline"/> &nbsp;AI总结
</n-button>
</n-input-group>
</div>
</template>
<style scoped>
</style>

View File

@ -1,374 +0,0 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {GetStockMoneyTrendByDay} from "../../wailsjs/go/main/App";
import * as echarts from "echarts";
const {code, name, darkTheme, days, chartHeight} = defineProps({
code: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
days: {
type: Number,
default: 14
},
chartHeight: {
type: Number,
default: 500
},
darkTheme: {
type: Boolean,
default: false
}
})
const LineChartRef = ref(null);
onMounted(
() => {
handleLine(code, days)
}
)
const handleLine = (code, days) => {
GetStockMoneyTrendByDay(code, days).then(result => {
//console.log("GetStockMoneyTrendByDay", result)
const chart = echarts.init(LineChartRef.value);
const categoryData = [];
const netamount_values = [];
const r0_net_values = [];
const trades_values = [];
let volume = []
let min = 0
let max = 0
for (let i = 0; i < result.length; i++) {
let resultElement = result[i]
categoryData.push(resultElement.opendate)
let netamount = (resultElement.netamount / 10000).toFixed(2);
netamount_values.push(netamount)
let price = Number(resultElement.trade);
trades_values.push(price)
r0_net_values.push((resultElement.r0_net / 10000).toFixed(2))
if (min === 0 || min > price) {
min = price
}
if (max < price) {
max = price
}
if (i > 0) {
let b = Number(Number(result[i].netamount) + Number(result[i - 1].netamount)) / 10000
volume.push(b.toFixed(2))
} else {
volume.push((Number(result[i].netamount) / 10000).toFixed(2))
}
}
//console.log("volume", volume)
const upColor = '#ec0000';
const downColor = '#00da3c';
let option = {
title: {
text: name,
left: '20px',
textStyle: {
color: darkTheme?'#ccc':'#456'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
lineStyle: {
color: '#376df4',
width: 1,
opacity: 1
}
},
borderWidth: 2,
borderColor: darkTheme?'#456':'#ccc',
backgroundColor: darkTheme?'#456':'#fff',
padding: 10,
textStyle: {
color: darkTheme?'#ccc':'#456'
},
},
axisPointer: {
link: [
{
xAxisIndex: 'all'
}
],
label: {
backgroundColor: '#888'
}
},
legend: {
show: true,
data: ['当日净流入', '主力当日净流入','累计净流入', '股价'],
selected: {
'当日净流入': true,
'主力当日净流入': true,
'累计净流入': true,
'股价': true,
},
//orient: 'vertical',
textStyle: {
color: darkTheme ? 'rgb(253,252,252)' : '#456'
},
right: 150,
},
dataZoom: [
{
type: 'inside',
xAxisIndex: [0, 1],
start: 86,
end: 100
},
{
show: true,
xAxisIndex: [0, 1],
type: 'slider',
top: '90%',
start: 86,
end: 100
}
],
grid: [
{
left: '8%',
right: '8%',
height: '50%',
},
{
left: '8%',
right: '8%',
top: '74%',
height: '15%'
},
],
xAxis: [
{
type: 'category',
data: categoryData,
axisPointer: {
z: 100
},
boundaryGap: false,
axisLine: { onZero: false },
splitLine: { show: false },
min: 'dataMin',
max: 'dataMax',
},
{
gridIndex: 1,
type: 'category',
data: categoryData,
axisLabel: {
show: false
},
}
],
yAxis: [
{
name: '当日净流入/万',
type: 'value',
axisLine: {
show: true
},
splitLine: {
show: false
},
},
{
name: '股价',
type: 'value',
min: min - 1,
max: max + 1,
minInterval: 0.01,
axisLine: {
show: true
},
splitLine: {
show: false
},
},
{
gridIndex: 1,
name: '累计净流入/万',
type: 'value',
axisLine: {
show: true
},
splitLine: {
show: false
},
},
],
series: [
{
yAxisIndex: 0,
name: '当日净流入',
data: netamount_values,
smooth: false,
showSymbol: false,
lineStyle: {
width: 2
},
markPoint: {
symbol: 'arrow',
symbolRotate: 90,
symbolSize: [10, 20],
symbolOffset: [10, 0],
itemStyle: {
color: '#0d7dfc'
},
label: {
position: 'right',
},
data: [
{type: 'max', name: 'Max'},
{type: 'min', name: 'Min'}
]
},
markLine: {
data: [
{
type: 'average',
name: 'Average',
lineStyle: {
color: '#0077ff',
width: 0.5
},
},
]
},
type: 'line'
},
{
yAxisIndex: 0,
name: '主力当日净流入',
data: r0_net_values,
smooth: false,
showSymbol: false,
lineStyle: {
width: 2
},
// markPoint: {
// symbol: 'arrow',
// symbolRotate: 90,
// symbolSize: [10, 20],
// symbolOffset: [10, 0],
// itemStyle: {
// color: '#0d7dfc'
// },
// label: {
// position: 'right',
// },
// data: [
// {type: 'max', name: 'Max'},
// {type: 'min', name: 'Min'}
// ]
// },
// markLine: {
// data: [
// {
// type: 'average',
// name: 'Average',
// lineStyle: {
// color: '#0077ff',
// width: 0.5
// },
// },
// ]
// },
type: 'bar'
},
{
yAxisIndex: 1,
name: '股价',
type: 'line',
data: trades_values,
smooth: true,
showSymbol: false,
lineStyle: {
width: 3
},
markPoint: {
symbol: 'arrow',
symbolRotate: 90,
symbolSize: [10, 20],
symbolOffset: [10, 0],
itemStyle: {
color: '#f39509'
},
label: {
position: 'right',
},
data: [
{type: 'max', name: 'Max'},
{type: 'min', name: 'Min'}
]
},
markLine: {
data: [
{
type: 'average',
name: 'Average',
lineStyle: {
color: '#f39509',
width: 0.5
},
},
]
},
},
{
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 2,
name: '累计净流入',
data: volume,
smooth: true,
showSymbol: false,
lineStyle: {
width: 2
},
markPoint: {
symbol: 'arrow',
symbolRotate: 90,
symbolSize: [10, 20],
symbolOffset: [10, 0],
// itemStyle: {
// color: '#f39509'
// },
label: {
position: 'right',
},
data: [
{type: 'max', name: 'Max'},
{type: 'min', name: 'Min'}
]
},
},
]
};
chart.setOption(option);
})
}
</script>
<template>
<div ref="LineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
</template>
<style scoped>
</style>

View File

@ -1,58 +0,0 @@
<script setup>
import {ReFleshTelegraphList} from "../../wailsjs/go/main/App";
import {RefreshCircle, RefreshCircleSharp, RefreshOutline} from "@vicons/ionicons5";
const { headerTitle,newsList } = defineProps({
headerTitle: {
type: String,
default: '市场资讯'
},
newsList: {
type: Array,
default: () => []
},
})
const emits = defineEmits(['update:message'])
const updateMessage = () => {
emits('update:message', headerTitle)
}
</script>
<template>
<n-list bordered>
<template #header>
<n-flex justify="space-between">
<n-tag :bordered="false" size="large" type="success" >{{ headerTitle }}</n-tag>
<n-button :bordered="false" @click="updateMessage"><n-icon color="#409EFF" size="25" :component="RefreshCircleSharp"/></n-button>
</n-flex>
</template>
<n-list-item v-for="item in newsList">
<n-space justify="start">
<n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'">
<n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>
{{ item.content }}
</n-text>
</n-space>
<n-space v-if="item.subjects" style="margin-top: 2px">
<n-tag :bordered="false" type="success" size="small" v-for="sub in item.subjects">
{{ sub }}
</n-tag>
<n-space v-if="item.stocks">
<n-tag :bordered="false" type="warning" size="small" v-for="sub in item.stocks">
{{ sub }}
</n-tag>
</n-space>
<n-tag v-if="item.url" :bordered="false" type="warning" size="small">
<a :href="item.url" target="_blank">
<n-text type="warning">查看原文</n-text>
</a>
</n-tag>
</n-space>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@ -1,101 +0,0 @@
<script setup>
import {CaretDown, CaretUp, RefreshCircleOutline} from "@vicons/ionicons5";
import {NText,useMessage} from "naive-ui";
import {onBeforeUnmount, onMounted, onUnmounted, ref} from "vue";
import {GetMoneyRankSina} from "../../wailsjs/go/main/App";
import KLineChart from "./KLineChart.vue";
const props = defineProps({
headerTitle: {
type: String,
default: '净流入额排名'
},
sort: {
type: String,
default: 'netamount'
},
})
const message = useMessage()
const dataList= ref([])
const sort = ref(props.sort)
const interval = ref(null)
onMounted(()=>{
sort.value=props.sort
GetMoneyRankSinaData()
interval.value=setInterval(()=>{
GetMoneyRankSinaData()
},1000*60)
})
onBeforeUnmount(()=>{
clearInterval(interval.value)
})
function GetMoneyRankSinaData(){
message.loading("正在刷新数据...")
GetMoneyRankSina(sort.value).then(result => {
if(result.length>0){
dataList.value = result
}
})
}
</script>
<template>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>代码</n-th>
<n-th>名称</n-th>
<n-th>最新价</n-th>
<n-th>涨跌幅</n-th>
<n-th>换手率</n-th>
<n-th>成交额/</n-th>
<n-th>流出资金/</n-th>
<n-th>流入资金/</n-th>
<n-th>净流入/</n-th>
<n-th>净流入率</n-th>
<n-th v-if="sort === 'r0_net'||sort==='r0_out'">主力流出/</n-th>
<n-th v-if="sort === 'r0_net'">主力流入/</n-th>
<n-th v-if="sort === 'r0_net'">主力净流入/</n-th>
<n-th >主力净流入率</n-th>
<n-th v-if="sort === 'r3_net'||sort==='r3_out'">散户流出/</n-th>
<n-th v-if="sort === 'r3_net'">散户流入/</n-th>
<n-th v-if="sort === 'r3_net'">散户净流入/</n-th>
<n-th >散户净流入率</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in dataList" :key="item.symbol">
<n-td><n-tag :bordered=false type="info">{{ item.symbol }}</n-tag></n-td>
<n-td>
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-button tag="a" text :type="item.changeratio>0?'error':'success'" :bordered=false >{{ item.name }}</n-button>
</template>
<k-line-chart style="width: 800px" :code="item.symbol" :chart-height="500" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{item.trade}}</n-text></n-td>
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{(item.changeratio*100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text :type="item.turnover>500?'error':'info'">{{(item.turnover/100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text type="info">{{(item.amount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info"> {{(item.outamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info"> {{(item.inamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info"> {{(item.netamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text :type="item.ratioamount>0?'error':'success'"> {{(item.ratioamount*100).toFixed(2)}}%</n-text></n-td>
<n-td v-if="sort === 'r0_net'||sort==='r0_out'"><n-text type="success"> {{(item.r0_out/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r0_net'"><n-text type="error"> {{(item.r0_in/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r0_net'"><n-text :type="item.r0_net>0?'error':'success'"> {{(item.r0_net/10000).toFixed(2)}}</n-text></n-td>
<n-td ><n-text :type="item.r0_ratio>0?'error':'success'"> {{(item.r0_ratio*100).toFixed(2)}}%</n-text></n-td>
<n-td v-if="sort === 'r3_net'||sort==='r3_out'"><n-text type="success"> {{(item.r3_out/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r3_net'"><n-text type="error"> {{(item.r3_in/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r3_net'"><n-text :type="item.r3_net>0?'error':'success'"> {{(item.r3_net/10000).toFixed(2)}}</n-text></n-td>
<n-td ><n-text :type="item.r3_ratio>0?'error':'success'"> {{(item.r3_ratio*100).toFixed(2)}}%</n-text></n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -1,17 +1,9 @@
<script setup> <script setup>
import {computed, onBeforeUnmount, onMounted, ref} from "vue"; import {onMounted, ref, watch} from "vue";
import { import {GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App";
AddPrompt, DelPrompt,
ExportConfig,
GetConfig,
GetPromptTemplates,
SendDingDingMessageByType,
UpdateConfig
} from "../../wailsjs/go/main/App";
import {useMessage} from "naive-ui"; import {useMessage} from "naive-ui";
import {data, models} from "../../wailsjs/go/models"; import {data} from "../../wailsjs/go/models";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage() const message = useMessage()
const formRef = ref(null) const formRef = ref(null)
@ -34,20 +26,9 @@ const formValue = ref({
model: 'deepseek-chat', model: 'deepseek-chat',
temperature: 0.1, temperature: 0.1,
maxTokens: 1024, maxTokens: 1024,
prompt:"",
timeout: 5,
questionTemplate: "{{stockName}}分析和总结",
crawlTimeOut:30,
kDays:30,
}, },
enableDanmu:false,
browserPath: '',
enableNews:false,
darkTheme:true,
enableFund:false,
enablePushNews:false,
}) })
const promptTemplates=ref([])
onMounted(()=>{ onMounted(()=>{
GetConfig().then(res=>{ GetConfig().then(res=>{
formValue.value.ID = res.ID formValue.value.ID = res.ID
@ -68,34 +49,14 @@ onMounted(()=>{
model:res.openAiModelName, model:res.openAiModelName,
temperature:res.openAiTemperature, temperature:res.openAiTemperature,
maxTokens:res.openAiMaxTokens, 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 console.log(res)
formValue.value.browserPath = res.browserPath
formValue.value.enableNews = res.enableNews
formValue.value.darkTheme = res.darkTheme
formValue.value.enableFund = res.enableFund
formValue.value.enablePushNews = res.enablePushNews
//console.log(res)
}) })
//message.info("") //message.info("")
})
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
})
})
onBeforeUnmount(() => {
message.destroyAll()
})
function saveConfig(){ function saveConfig(){
let config= new data.Settings({ let config= new data.Settings({
ID:formValue.value.ID, ID:formValue.value.ID,
dingPushEnable:formValue.value.dingPush.enable, dingPushEnable:formValue.value.dingPush.enable,
@ -109,25 +70,12 @@ function saveConfig(){
openAiModelName:formValue.value.openAI.model, openAiModelName:formValue.value.openAI.model,
openAiMaxTokens:formValue.value.openAI.maxTokens, openAiMaxTokens:formValue.value.openAI.maxTokens,
openAiTemperature:formValue.value.openAI.temperature, openAiTemperature:formValue.value.openAI.temperature,
tushareToken:formValue.value.tushareToken, 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,
enableFund:formValue.value.enableFund,
enablePushNews:formValue.value.enablePushNews
}) })
//console.log("Settings",config) //console.log("Settings",config)
UpdateConfig(config).then(res=>{ UpdateConfig(config).then(res=>{
message.success(res) message.success(res)
EventsEmit("updateSettings", config);
}) })
} }
@ -152,293 +100,79 @@ function sendTestNotice(){
message.info(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
formValue.value.enableFund = config.enableFund
formValue.value.enablePushNews = config.enablePushNews
// 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;
};
const showManagePromptsModal=ref(false)
const promptTypeOptions=[
{label:"模型系统Prompt",value:'模型系统Prompt'},
{label:"模型用户Prompt",value:'模型用户Prompt'},]
const formPromptRef=ref(null)
const formPrompt=ref({
ID:0,
Name:'',
Content:'',
Type:'',
})
function managePrompts(){
formPrompt.value.ID=0
showManagePromptsModal.value=true
}
function savePrompt(){
AddPrompt(formPrompt.value).then(res=>{
message.success(res)
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
})
showManagePromptsModal.value=false
})
}
function editPrompt(prompt){
//console.log(prompt)
formPrompt.value.ID=prompt.ID
formPrompt.value.Name=prompt.name
formPrompt.value.Content=prompt.content
formPrompt.value.Type=prompt.type
showManagePromptsModal.value=true
}
function deletePrompt(ID){
DelPrompt(ID).then(res=>{
message.success(res)
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
})
})
}
</script> </script>
<template> <template>
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px;"> <n-flex justify="left" style="margin-top: 12px;padding-left: 12px">
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" > <n-form ref="formRef" :model="formValue" :label-placement="'left'" :label-align="'left'" style="width: 100%;height: 100%">
<n-grid :cols="24" :x-gap="24" style="text-align: left" :layout-shift-disabled="true"> <n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24"> <n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text> <n-text type="default" style="font-size: 25px;font-weight: bold">基础设置</n-text>
</n-gi> </n-gi>
<n-form-item-gi :span="10" label="Tushare &nbsp;&nbsp;Token" path="tushareToken" > <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-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" > <n-form-item-gi :span="6" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
<n-switch v-model:value="formValue.updateBasicInfoOnStart" /> <n-switch v-model:value="formValue.updateBasicInfoOnStart" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="4" label="数据刷新间隔" path="refreshInterval" > <n-form-item-gi :span="6" label="数据刷新间隔(重启生效)" path="refreshInterval" >
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)"> <n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
<template #suffix> <template #suffix>
</template> </template>
</n-input-number> </n-input-number>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="6" label="暗黑主题:" path="darkTheme" >
<n-switch v-model:value="formValue.darkTheme" />
</n-form-item-gi>
<n-form-item-gi :span="10" label="浏览器安装路径:" path="browserPath" >
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
</n-form-item-gi>
<n-form-item-gi :span="6" label="指数基金:" path="enableFund" >
<n-switch v-model:value="formValue.enableFund" />
</n-form-item-gi>
</n-grid> </n-grid>
<n-grid :cols="24" :x-gap="24" style="text-align: left"> <n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24"> <n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text> <n-text type="default" style="font-size: 25px;font-weight: bold">通知设置</n-text>
</n-gi> </n-gi>
<n-form-item-gi :span="4" label="钉钉推送:" path="dingPush.enable" > <n-form-item-gi :span="6" label="是否启用钉钉推送:" path="dingPush.enable" >
<n-switch v-model:value="formValue.dingPush.enable" /> <n-switch v-model:value="formValue.dingPush.enable" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="4" label="本地推送:" path="localPush.enable" > <n-form-item-gi :span="6" label="是否启用本地推送:" path="localPush.enable" >
<n-switch v-model:value="formValue.localPush.enable" /> <n-switch v-model:value="formValue.localPush.enable" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="4" label="弹幕功能:" path="enableDanmu" > <n-form-item-gi :span="24" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
<n-switch v-model:value="formValue.enableDanmu" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="显示滚动快讯:" path="enableNews" >
<n-switch v-model:value="formValue.enableNews" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="市场资讯提醒:" path="enablePushNews" >
<n-switch v-model:value="formValue.enablePushNews" />
</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-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button> <n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
</n-form-item-gi> </n-form-item-gi>
</n-grid> </n-grid>
<n-grid :cols="24" :x-gap="24" style="text-align: left;"> <n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24"> <n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text> <n-text type="default" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
</n-gi> </n-gi>
<n-form-item-gi :span="3" label="AI诊股" path="openAI.enable" > <n-form-item-gi :span="6" label="是否启用AI诊股" path="openAI.enable" >
<n-switch v-model:value="formValue.openAI.enable" /> <n-switch v-model:value="formValue.openAI.enable" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="9" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl" > <n-form-item-gi :span="24" v-if="formValue.openAI.enable" label="openAI接口地址" path="openAI.baseUrl">
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable /> <n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="AI Timeout(秒)" title="AI请求超时时间(秒)" path="openAI.timeout" > <n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="apiKey" path="openAI.apiKey">
<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-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称" path="openAI.model" > <n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="AI模型" path="openAI.model">
<n-input type="text" placeholder="AI模型名称" v-model:value="formValue.openAI.model" clearable /> <n-input type="text" placeholder="AI模型" v-model:value="formValue.openAI.model" clearable />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI temperature" path="openAI.temperature" > <n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="temperature" path="openAI.temperature" >
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/> <n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="openAI maxTokens" path="openAI.maxTokens" > <n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="maxTokens" path="openAI.maxTokens">
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/> <n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
</n-form-item-gi> </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-grid>
<n-gi :span="24"> <n-gi :span="24">
<n-space justify="center"> <div style="display: flex; justify-content: center">
<n-button type="warning" @click="managePrompts">
添加提示词模板
</n-button>
<n-button type="primary" @click="saveConfig"> <n-button type="primary" @click="saveConfig">
保存 保存
</n-button> </n-button>
<n-button type="info" @click="exportConfig"> </div>
导出
</n-button>
<n-button type="error" @click="importConfig">
导入
</n-button>
</n-space>
</n-gi> </n-gi>
</n-form> </n-form>
<n-gi :span="24" v-if="promptTemplates.length>0" v-for="prompt in promptTemplates" >
<n-flex justify="start">
<n-tag closable @close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content" :type="prompt.type==='模型系统Prompt'?'success':'info'" :bordered="false"> {{prompt.name}} </n-tag>
</n-flex> </n-flex>
</n-gi>
</n-flex>
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
<n-card
style="width: 800px;height: 600px;text-align: left"
:bordered="false"
:title="(formPrompt.ID>0?'修改':'添加')+'提示词'"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'" >
<n-form-item label="名称">
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称" />
</n-form-item>
<n-form-item label="类型">
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型" />
</n-form-item>
<n-form-item label="内容">
<n-input v-model:value="formPrompt.Content"
type="textarea"
:show-count="true"
placeholder="请输入prompt"
:autosize="{
minRows: 12,
maxRows: 12,
}"
/>
</n-form-item>
</n-form>
<template #footer>
<n-flex justify="end">
<n-button type="primary" @click="savePrompt">
保存
</n-button>
<n-button type="warning" @click="showManagePromptsModal=false">
取消
</n-button>
</n-flex>
</template>
</n-card>
</n-modal>
</template> </template>
<style scoped> <style scoped>

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import router from './router/router'
const app = createApp(App) const app = createApp(App)
app.use(router) app.use(router)
app.use(naive) app.use(naive)
app.mount('#app') app.mount('#app')

View File

@ -1,22 +1,15 @@
import {createMemoryHistory, createRouter, createWebHashHistory, createWebHistory} from 'vue-router' import { createMemoryHistory, createRouter } from 'vue-router'
import stockView from '../components/stock.vue' import stockView from '../components/stock.vue'
import settingsView from '../components/settings.vue' import settingsView from '../components/settings.vue'
import about from "../components/about.vue";
import fundView from "../components/fund.vue";
import market from "../components/market.vue";
const routes = [ const routes = [
{ path: '/', component: stockView,name: 'stock'}, { path: '/', component: stockView,name: 'stock' },
{ path: '/fund', component: fundView,name: 'fund' }, { path: '/settings/:id', component: settingsView,name: 'settings' },
{ path: '/settings', component: settingsView,name: 'settings' },
{ path: '/about', component: about,name: 'about' },
{ path: '/market', component: market,name: 'market' },
] ]
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createMemoryHistory(),
routes, routes,
}) })

View File

@ -1,12 +1,12 @@
html { html {
/*background-color: rgba(27, 38, 54, 1);*/ background-color: rgba(27, 38, 54, 1);
text-align: center; text-align: center;
/*color: white;*/ color: white;
} }
body { body {
margin: 0; margin: 0;
/*color: white;*/ color: white;
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif; sans-serif;

View File

@ -1,99 +1,20 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
import {data} from '../models'; import {data} from '../models';
import {models} from '../models';
export function AddCronTask(arg1:data.FollowedStock):Promise<any>;
export function AddGroup(arg1:data.Group):Promise<string>;
export function AddPrompt(arg1:models.Prompt):Promise<string>;
export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
export function CheckUpdate():Promise<void>;
export function ClsCalendar():Promise<Array<any>>;
export function DelPrompt(arg1:number):Promise<string>;
export function EMDictCode(arg1:string):Promise<Array<any>>;
export function ExportConfig():Promise<string>;
export function Follow(arg1:string):Promise<string>; export function Follow(arg1:string):Promise<string>;
export function FollowFund(arg1:string):Promise<string>;
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
export function GetConfig():Promise<data.Settings>; export function GetConfig():Promise<data.Settings>;
export function GetFollowList(arg1:number):Promise<any>; export function GetFollowList():Promise<Array<data.FollowedStock>>;
export function GetFollowedFund():Promise<Array<data.FollowedFund>>;
export function GetGroupList():Promise<Array<data.Group>>;
export function GetGroupStockList(arg1:number):Promise<Array<data.GroupStock>>;
export function GetIndustryMoneyRankSina(arg1:string,arg2:string):Promise<Array<Record<string, any>>>;
export function GetIndustryRank(arg1:string,arg2:number):Promise<Array<any>>;
export function GetMoneyRankSina(arg1:string):Promise<Array<Record<string, any>>>;
export function GetPromptTemplates(arg1:string,arg2:string):Promise<any>;
export function GetStockCommonKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
export function GetStockKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>; export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
export function GetStockMinutePriceLineData(arg1:string,arg2:string):Promise<Record<string, any>>;
export function GetStockMoneyTrendByDay(arg1:string,arg2:number):Promise<Array<Record<string, any>>>;
export function GetTelegraphList(arg1:string):Promise<any>;
export function GetVersionInfo():Promise<models.VersionInfo>;
export function GetfundList(arg1:string):Promise<Array<data.FundBasic>>;
export function GlobalStockIndexes():Promise<Record<string, any>>;
export function Greet(arg1:string):Promise<data.StockInfo>; export function Greet(arg1:string):Promise<data.StockInfo>;
export function HotEvent(arg1:number):Promise<any>; export function NewChat(arg1:string):Promise<string>;
export function HotStock(arg1:string):Promise<any>; export function NewChatStream(arg1:string,arg2:string):Promise<void>;
export function HotTopic(arg1:number):Promise<Array<any>>;
export function IndustryResearchReport(arg1:string):Promise<Array<any>>;
export function InvestCalendarTimeLine(arg1:string):Promise<Array<any>>;
export function LongTigerRank(arg1:string):Promise<any>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
export function NewsPush(arg1:any):Promise<void>;
export function ReFleshTelegraphList(arg1:string):Promise<any>;
export function RemoveGroup(arg1:number):Promise<string>;
export function RemoveStockGroup(arg1:string,arg2:string,arg3:number):Promise<string>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;
export function SearchStock(arg1:string):Promise<Record<string, any>>;
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>; export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
@ -103,20 +24,8 @@ export function SetAlarmChangePercent(arg1:number,arg2:number,arg3:string):Promi
export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>; export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>;
export function SetStockAICron(arg1:string,arg2:string):Promise<void>;
export function SetStockSort(arg1:number,arg2:string):Promise<void>; export function SetStockSort(arg1:number,arg2:string):Promise<void>;
export function ShareAnalysis(arg1:string,arg2:string):Promise<string>;
export function StockNotice(arg1:string):Promise<Array<any>>;
export function StockResearchReport(arg1:string):Promise<Array<any>>;
export function SummaryStockNews(arg1:string,arg2:any):Promise<void>;
export function UnFollow(arg1: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>; export function UpdateConfig(arg1:data.Settings):Promise<string>;

View File

@ -2,188 +2,32 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
export function AddCronTask(arg1) {
return window['go']['main']['App']['AddCronTask'](arg1);
}
export function AddGroup(arg1) {
return window['go']['main']['App']['AddGroup'](arg1);
}
export function AddPrompt(arg1) {
return window['go']['main']['App']['AddPrompt'](arg1);
}
export function AddStockGroup(arg1, arg2) {
return window['go']['main']['App']['AddStockGroup'](arg1, arg2);
}
export function AnalyzeSentiment(arg1) {
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
}
export function CheckUpdate() {
return window['go']['main']['App']['CheckUpdate']();
}
export function ClsCalendar() {
return window['go']['main']['App']['ClsCalendar']();
}
export function DelPrompt(arg1) {
return window['go']['main']['App']['DelPrompt'](arg1);
}
export function EMDictCode(arg1) {
return window['go']['main']['App']['EMDictCode'](arg1);
}
export function ExportConfig() {
return window['go']['main']['App']['ExportConfig']();
}
export function Follow(arg1) { export function Follow(arg1) {
return window['go']['main']['App']['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() { export function GetConfig() {
return window['go']['main']['App']['GetConfig'](); return window['go']['main']['App']['GetConfig']();
} }
export function GetFollowList(arg1) { export function GetFollowList() {
return window['go']['main']['App']['GetFollowList'](arg1); return window['go']['main']['App']['GetFollowList']();
}
export function GetFollowedFund() {
return window['go']['main']['App']['GetFollowedFund']();
}
export function GetGroupList() {
return window['go']['main']['App']['GetGroupList']();
}
export function GetGroupStockList(arg1) {
return window['go']['main']['App']['GetGroupStockList'](arg1);
}
export function GetIndustryMoneyRankSina(arg1, arg2) {
return window['go']['main']['App']['GetIndustryMoneyRankSina'](arg1, arg2);
}
export function GetIndustryRank(arg1, arg2) {
return window['go']['main']['App']['GetIndustryRank'](arg1, arg2);
}
export function GetMoneyRankSina(arg1) {
return window['go']['main']['App']['GetMoneyRankSina'](arg1);
}
export function GetPromptTemplates(arg1, arg2) {
return window['go']['main']['App']['GetPromptTemplates'](arg1, arg2);
}
export function GetStockCommonKLine(arg1, arg2, arg3) {
return window['go']['main']['App']['GetStockCommonKLine'](arg1, arg2, arg3);
}
export function GetStockKLine(arg1, arg2, arg3) {
return window['go']['main']['App']['GetStockKLine'](arg1, arg2, arg3);
} }
export function GetStockList(arg1) { export function GetStockList(arg1) {
return window['go']['main']['App']['GetStockList'](arg1); return window['go']['main']['App']['GetStockList'](arg1);
} }
export function GetStockMinutePriceLineData(arg1, arg2) {
return window['go']['main']['App']['GetStockMinutePriceLineData'](arg1, arg2);
}
export function GetStockMoneyTrendByDay(arg1, arg2) {
return window['go']['main']['App']['GetStockMoneyTrendByDay'](arg1, arg2);
}
export function GetTelegraphList(arg1) {
return window['go']['main']['App']['GetTelegraphList'](arg1);
}
export function GetVersionInfo() {
return window['go']['main']['App']['GetVersionInfo']();
}
export function GetfundList(arg1) {
return window['go']['main']['App']['GetfundList'](arg1);
}
export function GlobalStockIndexes() {
return window['go']['main']['App']['GlobalStockIndexes']();
}
export function Greet(arg1) { export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1); return window['go']['main']['App']['Greet'](arg1);
} }
export function HotEvent(arg1) { export function NewChat(arg1) {
return window['go']['main']['App']['HotEvent'](arg1); return window['go']['main']['App']['NewChat'](arg1);
} }
export function HotStock(arg1) { export function NewChatStream(arg1, arg2) {
return window['go']['main']['App']['HotStock'](arg1); return window['go']['main']['App']['NewChatStream'](arg1, arg2);
}
export function HotTopic(arg1) {
return window['go']['main']['App']['HotTopic'](arg1);
}
export function IndustryResearchReport(arg1) {
return window['go']['main']['App']['IndustryResearchReport'](arg1);
}
export function InvestCalendarTimeLine(arg1) {
return window['go']['main']['App']['InvestCalendarTimeLine'](arg1);
}
export function LongTigerRank(arg1) {
return window['go']['main']['App']['LongTigerRank'](arg1);
}
export function NewChatStream(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
}
export function NewsPush(arg1) {
return window['go']['main']['App']['NewsPush'](arg1);
}
export function ReFleshTelegraphList(arg1) {
return window['go']['main']['App']['ReFleshTelegraphList'](arg1);
}
export function RemoveGroup(arg1) {
return window['go']['main']['App']['RemoveGroup'](arg1);
}
export function RemoveStockGroup(arg1, arg2, arg3) {
return window['go']['main']['App']['RemoveStockGroup'](arg1, arg2, arg3);
}
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
}
export function SaveAsMarkdown(arg1, arg2) {
return window['go']['main']['App']['SaveAsMarkdown'](arg1, arg2);
}
export function SearchStock(arg1) {
return window['go']['main']['App']['SearchStock'](arg1);
} }
export function SendDingDingMessage(arg1, arg2) { export function SendDingDingMessage(arg1, arg2) {
@ -202,38 +46,14 @@ export function SetCostPriceAndVolume(arg1, arg2, arg3) {
return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3); return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3);
} }
export function SetStockAICron(arg1, arg2) {
return window['go']['main']['App']['SetStockAICron'](arg1, arg2);
}
export function SetStockSort(arg1, arg2) { export function SetStockSort(arg1, arg2) {
return window['go']['main']['App']['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 StockNotice(arg1) {
return window['go']['main']['App']['StockNotice'](arg1);
}
export function StockResearchReport(arg1) {
return window['go']['main']['App']['StockResearchReport'](arg1);
}
export function SummaryStockNews(arg1, arg2) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2);
}
export function UnFollow(arg1) { export function UnFollow(arg1) {
return window['go']['main']['App']['UnFollow'](arg1); return window['go']['main']['App']['UnFollow'](arg1);
} }
export function UnFollowFund(arg1) {
return window['go']['main']['App']['UnFollowFund'](arg1);
}
export function UpdateConfig(arg1) { export function UpdateConfig(arg1) {
return window['go']['main']['App']['UpdateConfig'](arg1); return window['go']['main']['App']['UpdateConfig'](arg1);
} }

View File

@ -1,235 +1,5 @@
export namespace data { export namespace data {
export class FundBasic {
ID: number;
// Go type: time
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 FundBasic(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.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 Group {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
name: string;
sort: number;
static createFrom(source: any = {}) {
return new Group(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.name = source["name"];
this.sort = source["sort"];
}
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 GroupStock {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
stockCode: string;
groupId: number;
groupInfo: Group;
static createFrom(source: any = {}) {
return new GroupStock(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.stockCode = source["stockCode"];
this.groupId = source["groupId"];
this.groupInfo = this.convertValues(source["groupInfo"], Group);
}
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 FollowedStock { export class FollowedStock {
StockCode: string; StockCode: string;
Name: string; Name: string;
@ -243,9 +13,7 @@ export namespace data {
// Go type: time // Go type: time
Time: any; Time: any;
Sort: number; Sort: number;
Cron?: string;
IsDel: number; IsDel: number;
Groups: GroupStock[];
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new FollowedStock(source); return new FollowedStock(source);
@ -264,9 +32,7 @@ export namespace data {
this.AlarmPrice = source["AlarmPrice"]; this.AlarmPrice = source["AlarmPrice"];
this.Time = this.convertValues(source["Time"], null); this.Time = this.convertValues(source["Time"], null);
this.Sort = source["Sort"]; this.Sort = source["Sort"];
this.Cron = source["Cron"];
this.IsDel = source["IsDel"]; this.IsDel = source["IsDel"];
this.Groups = this.convertValues(source["Groups"], GroupStock);
} }
convertValues(a: any, classs: any, asMap: boolean = false): any { convertValues(a: any, classs: any, asMap: boolean = false): any {
@ -287,29 +53,6 @@ export namespace data {
return a; return a;
} }
} }
export class SentimentResult {
Score: number;
Category: number;
PositiveCount: number;
NegativeCount: number;
Description: string;
static createFrom(source: any = {}) {
return new SentimentResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.Score = source["Score"];
this.Category = source["Category"];
this.PositiveCount = source["PositiveCount"];
this.NegativeCount = source["NegativeCount"];
this.Description = source["Description"];
}
}
export class Settings { export class Settings {
ID: number; ID: number;
// Go type: time // Go type: time
@ -330,19 +73,6 @@ export namespace data {
openAiModelName: string; openAiModelName: string;
openAiMaxTokens: number; openAiMaxTokens: number;
openAiTemperature: number; openAiTemperature: number;
openAiApiTimeOut: number;
prompt: string;
checkUpdate: boolean;
questionTemplate: string;
crawlTimeOut: number;
kDays: number;
enableDanmu: boolean;
browserPath: string;
enableNews: boolean;
darkTheme: boolean;
browserPoolSize: number;
enableFund: boolean;
enablePushNews: boolean;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new Settings(source); return new Settings(source);
@ -366,19 +96,6 @@ export namespace data {
this.openAiModelName = source["openAiModelName"]; this.openAiModelName = source["openAiModelName"];
this.openAiMaxTokens = source["openAiMaxTokens"]; this.openAiMaxTokens = source["openAiMaxTokens"];
this.openAiTemperature = source["openAiTemperature"]; 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"];
this.browserPoolSize = source["browserPoolSize"];
this.enableFund = source["enableFund"];
this.enablePushNews = source["enablePushNews"];
} }
convertValues(a: any, classs: any, asMap: boolean = false): any { convertValues(a: any, classs: any, asMap: boolean = false): any {
@ -514,9 +231,6 @@ export namespace data {
"卖四申报": string; "卖四申报": string;
"卖五报价": string; "卖五报价": string;
"卖五申报": string; "卖五申报": string;
"市场": string;
"盘前盘后": string;
"盘前盘后涨跌幅": string;
changePercent: number; changePercent: number;
changePrice: number; changePrice: number;
highRate: number; highRate: number;
@ -529,7 +243,6 @@ export namespace data {
sort: number; sort: number;
alarmChangePercent: number; alarmChangePercent: number;
alarmPrice: number; alarmPrice: number;
Groups: GroupStock[];
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new StockInfo(source); return new StockInfo(source);
@ -575,9 +288,6 @@ export namespace data {
this["卖四申报"] = source["卖四申报"]; this["卖四申报"] = source["卖四申报"];
this["卖五报价"] = source["卖五报价"]; this["卖五报价"] = source["卖五报价"];
this["卖五申报"] = source["卖五申报"]; this["卖五申报"] = source["卖五申报"];
this["市场"] = source["市场"];
this["盘前盘后"] = source["盘前盘后"];
this["盘前盘后涨跌幅"] = source["盘前盘后涨跌幅"];
this.changePercent = source["changePercent"]; this.changePercent = source["changePercent"];
this.changePrice = source["changePrice"]; this.changePrice = source["changePrice"];
this.highRate = source["highRate"]; this.highRate = source["highRate"];
@ -590,136 +300,6 @@ export namespace data {
this.sort = source["sort"]; this.sort = source["sort"];
this.alarmChangePercent = source["alarmChangePercent"]; this.alarmChangePercent = source["alarmChangePercent"];
this.alarmPrice = source["alarmPrice"]; this.alarmPrice = source["alarmPrice"];
this.Groups = this.convertValues(source["Groups"], GroupStock);
}
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 Prompt {
ID: number;
name: string;
content: string;
type: string;
static createFrom(source: any = {}) {
return new Prompt(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.name = source["name"];
this.content = source["content"];
this.type = source["type"];
}
}
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 { convertValues(a: any, classs: any, asMap: boolean = false): any {

View File

@ -134,7 +134,7 @@ export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window. // Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): void; export function WindowSetSize(width: number, height: number): Promise<Size>;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window. // Gets the width and height of the window.

63
go.mod
View File

@ -1,26 +1,21 @@
module go-stock module go-stock
go 1.23.0 go 1.23
toolchain go1.23.0
require ( require (
github.com/PuerkitoBio/goquery v1.10.1 github.com/PuerkitoBio/goquery v1.10.1
github.com/chromedp/chromedp v0.11.2 github.com/chromedp/chromedp v0.11.2
github.com/coocood/freecache v1.2.4 github.com/coocood/freecache v1.2.4
github.com/duke-git/lancet/v2 v2.3.4 github.com/duke-git/lancet/v2 v2.3.4
github.com/energye/systray v1.0.2 github.com/getlantern/systray v1.2.2
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-ego/gse v0.80.3
github.com/go-resty/resty/v2 v2.16.2 github.com/go-resty/resty/v2 v2.16.2
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/robertkrimen/otto v0.5.1 github.com/wailsapp/wails/v2 v2.9.2
github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.49.1
github.com/stretchr/testify v1.10.0
github.com/tidwall/gjson v1.14.2
github.com/wailsapp/wails/v2 v2.10.1
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/sys v0.31.0 golang.org/x/text v0.21.0
golang.org/x/text v0.23.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/gorm v1.25.12 gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3 gorm.io/plugin/dbresolver v1.5.3
@ -33,49 +28,51 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect
github.com/chromedp/sysutil v1.1.0 // 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/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/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect github.com/labstack/echo/v4 v4.10.2 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect github.com/leaanthony/gosod v1.0.3 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.1 // indirect github.com/leaanthony/u v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect github.com/samber/lo v1.38.1 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tkrajina/go-reflector v0.5.6 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vcaesar/cedar v0.20.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 github.com/wailsapp/mimetype v1.4.1 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.36.0 // indirect golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.38.0 // indirect golang.org/x/net v0.33.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect golang.org/x/sys v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect

147
go.sum
View File

@ -14,26 +14,39 @@ github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipw
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= 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 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls= github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= 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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/energye/systray v1.0.2 h1:63R4prQkANtpM2CIA4UrDCuwZFt+FiygG77JYCsNmXc= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/energye/systray v1.0.2/go.mod h1:sp7Q/q/I4/w5ebvpSuJVep71s9Bg7L9ZVp69gBASehM= 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 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= 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 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-ego/gse v0.80.3 h1:YNFkjMhlhQnUeuoFcUEd1ivh6SOB764rT8GDsEbDiEg= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ego/gse v0.80.3/go.mod h1:Gt3A9Ry1Eso2Kza4MRaiZ7f2DTAvActmETY46Lxg0gU= 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 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= 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 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 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 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= 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 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@ -42,14 +55,13 @@ 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/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 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 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/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/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 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.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 h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= 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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -60,45 +72,47 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 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/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
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 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= 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/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= 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/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= github.com/leaanthony/u v1.1.0/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 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= 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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/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.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 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 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 h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 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 h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -107,40 +121,29 @@ 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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NXpcvv2E4I7+ii6L5hZO2S6C3kw= github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 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 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vcaesar/cedar v0.20.2 h1:TDx7AdZhilKcfE1WvdToTJf5VrC/FXcUOW+KY1upLZ4= github.com/wailsapp/go-webview2 v1.0.16 h1:wffnvnkkLvhRex/aOrA3R7FP7rkvOqL/bir1br7BekU=
github.com/vcaesar/cedar v0.20.2/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik= github.com/wailsapp/go-webview2 v1.0.16/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
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 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns= github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY= github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@ -153,9 +156,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= 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/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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -172,9 +174,8 @@ 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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -183,11 +184,16 @@ 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.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-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-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-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-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-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-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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -198,9 +204,8 @@ 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.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.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.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -219,11 +224,10 @@ 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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -231,13 +235,12 @@ 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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= 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 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 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=

BIN
img.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 40 KiB

248
main.go
View File

@ -1,11 +1,9 @@
package main package main
import ( import (
"context"
"embed" "embed"
"encoding/json" "encoding/json"
"fmt" "github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/slice"
"github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger" "github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu"
@ -16,12 +14,9 @@ import (
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data" "go-stock/backend/data"
"go-stock/backend/db" "go-stock/backend/db"
log "go-stock/backend/logger" "log"
"go-stock/backend/models"
"os" "os"
goruntime "runtime" goruntime "runtime"
"runtime/debug"
"strings"
"time" "time"
) )
@ -34,35 +29,23 @@ var icon []byte
//go:embed build/app.ico //go:embed build/app.ico
var icon2 []byte 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 //go:embed build/stock_basic.json
var stocksBin []byte 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 //go:generate cp -R ./data ./build/bin
var Version string
var VersionCommit string
func main() { func main() {
checkDir("data") checkDir("data")
db.Init("") db.Init("")
go AutoMigrate() 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.Model(&data.Group{}).Where("id = ?", 0).FirstOrCreate(&data.Group{ if stocksBin != nil && len(stocksBin) > 0 {
// Name: "默认分组", go initStockData()
// Sort: 0, }
//}) updateBasicInfo()
// Create an instance of the app structure // Create an instance of the app structure
app := NewApp() app := NewApp()
@ -99,41 +82,22 @@ func main() {
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) { //FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
// runtime.Quit(app.ctx) // 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, _, 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 // Create application with options
err = wails.Run(&options.App{ err := wails.Run(&options.App{
Title: "go-stock", Title: "go-stock",
Width: width * 4 / 5, Width: 1366,
Height: 900, Height: 920,
MinWidth: 1456, MinWidth: 1024,
MinHeight: 768, MinHeight: 768,
//MaxWidth: width, MaxWidth: 1280,
//MaxHeight: height, MaxHeight: 960,
DisableResize: false, DisableResize: false,
Fullscreen: false, Fullscreen: false,
Frameless: true, Frameless: true,
StartHidden: false, StartHidden: false,
HideWindowOnClose: false, HideWindowOnClose: false,
EnableDefaultContextMenu: true, BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
BackgroundColour: backgroundColour,
Assets: assets, Assets: assets,
Menu: AppMenu, Menu: AppMenu,
Logger: nil, Logger: nil,
@ -144,10 +108,6 @@ func main() {
OnBeforeClose: app.beforeClose, OnBeforeClose: app.beforeClose,
OnShutdown: app.shutdown, OnShutdown: app.shutdown,
WindowStartState: options.Normal, WindowStartState: options.Normal,
SingleInstanceLock: &options.SingleInstanceLock{
UniqueId: "go-stock",
OnSecondInstanceLaunch: OnSecondInstanceLaunch,
},
Bind: []interface{}{ Bind: []interface{}{
app, app,
}, },
@ -181,80 +141,7 @@ func main() {
}) })
if err != nil { if err != nil {
log.SugaredLogger.Fatal(err) log.Fatal(err)
}
}
func AutoMigrate() {
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{})
db.Dao.AutoMigrate(&models.PromptTemplate{})
db.Dao.AutoMigrate(&data.Group{})
db.Dao.AutoMigrate(&data.GroupStock{})
db.Dao.AutoMigrate(&models.Tags{})
db.Dao.AutoMigrate(&models.Telegraph{})
db.Dao.AutoMigrate(&models.TelegraphTags{})
db.Dao.AutoMigrate(&models.LongTigerRankData{})
}
func initStockDataUS(ctx context.Context) {
defer func() {
go runtime.EventsEmit(ctx, "loadingMsg", "done")
}()
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))
var total int64
db.Dao.Model(&models.StockInfoUS{}).Count(&total)
if total != int64(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(ctx context.Context) {
defer func() {
go runtime.EventsEmit(ctx, "loadingMsg", "done")
}()
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))
var total int64
db.Dao.Model(&models.StockInfoHK{}).Count(&total)
if total != int64(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)
}
} }
} }
@ -268,89 +155,46 @@ func updateBasicInfo() {
} }
} }
func initStockData(ctx context.Context) { func initStockData() {
defer func() { var count int64
go runtime.EventsEmit(ctx, "loadingMsg", "done") db.Dao.Model(&data.StockBasic{}).Count(&count)
}() if count > 0 {
fields := "ts_code,symbol,name,area,industry,cnspell,market,list_date,act_name,act_ent_type,fullname,exchange,list_status,curr_type,enname,delist_date,is_hs" return
log.SugaredLogger.Info("init stock data") }
logger.NewDefaultLogger().Info("init stock data")
res := &data.TushareStockBasicResponse{} res := &data.TushareStockBasicResponse{}
err := json.Unmarshal(stocksBin, res) err := json.Unmarshal(stocksBin, res)
if err != nil { if err != nil {
log.SugaredLogger.Error(err.Error()) logger.NewDefaultLogger().Error(err.Error())
return return
} }
for _, item := range res.Data.Items { for _, item := range res.Data.Items {
stock := &data.StockBasic{} stock := &data.StockBasic{}
stockData := map[string]any{} stock.Exchange = convertor.ToString(item[0])
for _, field := range strings.Split(fields, ",") { stock.IsHs = convertor.ToString(item[1])
//logger.SugaredLogger.Infof("field: %s", field) stock.Name = convertor.ToString(item[2])
idx := slice.IndexOf(res.Data.Fields, field) stock.Industry = convertor.ToString(item[3])
if idx == -1 { stock.ListStatus = convertor.ToString(item[4])
continue stock.ActName = convertor.ToString(item[5])
stock.ID = uint(item[6].(float64))
stock.CurrType = convertor.ToString(item[7])
stock.Area = convertor.ToString(item[8])
stock.ListDate = convertor.ToString(item[9])
stock.DelistDate = convertor.ToString(item[10])
stock.ActEntType = convertor.ToString(item[11])
stock.TsCode = convertor.ToString(item[12])
stock.Symbol = convertor.ToString(item[13])
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)
} }
stockData[field] = item[idx]
}
jsonData, _ := json.Marshal(stockData)
err := json.Unmarshal(jsonData, stock)
if err != nil {
continue
}
stock.ID = 0
var count int64
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Count(&count)
if count > 0 {
continue
} else {
db.Dao.Create(stock)
}
//db.Dao.Model(&data.StockBasic{}).FirstOrCreate(stock, &data.StockBasic{TsCode: stock.TsCode}).Where("ts_code = ?", stock.TsCode).Updates(stock)
}
//for _, item := range res.Data.Items {
// stock := &data.StockBasic{}
// stock.Exchange = convertor.ToString(item[0])
// stock.IsHs = convertor.ToString(item[1])
// stock.Name = convertor.ToString(item[2])
// stock.Industry = convertor.ToString(item[3])
// stock.ListStatus = convertor.ToString(item[4])
// stock.ActName = convertor.ToString(item[5])
// stock.ID = uint(item[6].(float64))
// stock.CurrType = convertor.ToString(item[7])
// stock.Area = convertor.ToString(item[8])
// stock.ListDate = convertor.ToString(item[9])
// stock.DelistDate = convertor.ToString(item[10])
// stock.ActEntType = convertor.ToString(item[11])
// stock.TsCode = convertor.ToString(item[12])
// stock.Symbol = convertor.ToString(item[13])
// stock.Cnspell = convertor.ToString(item[14])
// stock.Fullname = convertor.ToString(item[20])
// stock.Ename = convertor.ToString(item[21])
//
// var count int64
// db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Count(&count)
// if count > 0 {
// continue
// } else {
// db.Dao.Create(stock)
// }
//}
} }
func checkDir(dir string) { func checkDir(dir string) {
_, err := os.Stat(dir) _, err := os.Stat(dir)
if os.IsNotExist(err) { if os.IsNotExist(err) {
os.Mkdir(dir, os.ModePerm) os.Mkdir(dir, os.ModePerm)
log.SugaredLogger.Info("create dir: " + dir) logger.NewDefaultLogger().Info("create dir: " + dir)
}
}
// PanicHandler 捕获 panic 的包装函数
func PanicHandler() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
debug.PrintStack()
} }
} }

View File

@ -14,6 +14,6 @@
"productName": "go-stock", "productName": "go-stock",
"productVersion": "1.0.0", "productVersion": "1.0.0",
"copyright": "Copyright#sparkmemory@163.com", "copyright": "Copyright#sparkmemory@163.com",
"comments": "股票行情实时获取,AI赋能分析股票,支持DeepSeekOpenAI OllamaLMStudioAnythingLLM硅基流动火山方舟阿里云百炼等平台或模型。软件发行版见GitHubhttps://github.com/ArvinLovegood/go-stock" "comments": "股票行情实时获取,AI赋能分析股票"
} }
} }