Compare commits

..

270 Commits

Author SHA1 Message Date
ArvinLovegood
b2b0300aa1 feat(data):增加对美股数据的支持
- 新增 getUSStockPriceInfo 函数用于获取美股实时行情信息
- 修改 SearchStockPriceInfo 函数,支持美股代码查询
- 更新 Tushare 数据接口,增加对美股每日数据的支持
- 优化股票代码处理逻辑,兼容不同市场代码格式
2025-02-28 17:38:48 +08:00
ArvinLovegood
dbc25ca582 feat(README): 更新功能状态和版本日志
- 将美股支持状态从 🚧 修改为 
- 添加 2025.02.28 美股数据支持到更新日志
2025-02-28 16:45:18 +08:00
ArvinLovegood
40a4e58276 chore: 更新 .gitignore 文件
- 移除 frontend/package.json.md5 文件的忽略规则
- 添加 /build/us.json 文件到忽略列表
2025-02-28 16:38:15 +08:00
ArvinLovegood
2c2d689f53 feat(stock):添加美国股票基本信息初始化
- 增加美国股票基本信息的 JSON 文件
- 实现 initStockDataUS 函数用于初始化美国股票数据
- 在主程序中添加美国股票数据初始化的逻辑
2025-02-28 16:35:43 +08:00
ArvinLovegood
fdca30ce3a feat(stock):添加美股数据支持
- 新增 StockInfoUS 模型用于存储美股信息
- 实现 IsUSTradingTime 函数判断美股交易时间
- 修改 MonitorStockPrices 函数以支持美股数据
- 更新前端股票组件以适配美股数据
- 优化后端 API 以支持美股实时数据获取和解析
2025-02-28 16:30:48 +08:00
ArvinLovegood
7b3bad4102 feat(frontend):添加软件更新检查功能
- 在 about.vue 中添加检查更新按钮和相关逻辑
- 在 App.d.ts 和 App.js 中添加 CheckUpdate 函数声明
- 在 app.go 中实现 CheckUpdate 方法,检查 GitHub 上的最新版本- 更新 go.mod 中的依赖版本
2025-02-27 21:23:25 +08:00
Lovegood
531b01bca3 Update issue templates 2025-02-27 14:07:24 +08:00
ArvinLovegood
645c6979a4 docs: 添加 Pull Request 模板
添加了 .github/pull_request_template.md 文件,用于规范 Pull Request 的提交信息。模板包含了以下内容:
- PR 概述
- 相关问题
- 改动内容详细说明(代码修改、新增功能、删除内容)
-测试情况(单元测试、集成测试)
- 注意事项
- 其他补充说明

此模板有助于提高 PR 的质量和可审查性,确保开发者在提交 PR 时提供足够的信息。
2025-02-27 13:59:09 +08:00
ArvinLovegood
5c94b40e4d docs(README): 更新大模型聚合平台信息并添加火山方舟链接
- 在大模型聚合平台部分添加了火山方舟的注册链接
- 更新了硅基流动的注册链接
- 增加了火山方舟的相关描述
2025-02-27 09:07:52 +08:00
ArvinLovegood
83603a12a7 feat(frontend):设置页面添加弹幕功能开关
(今天看见某位朋友在弹幕中说,关掉弹幕。那就如你所愿,你可以自己决定是否显示弹幕了😎)
- 在设置页面添加弹幕功能开关
- 调整数据刷新间隔和启动时更新信息的布局
- 在股票页面实现弹幕功能,根据设置开关控制是否显示弹幕
- 调整应用窗口高度比例
- 优化 OpenAI API 请求时的 URL 处理
2025-02-26 22:19:44 +08:00
ArvinLovegood
2aba86e424 feat(frontend):设置页面添加弹幕功能开关
(今天看见某位朋友在弹幕中说,关掉弹幕。那就如你所愿,你可以自己决定是否显示弹幕了😎)
- 在设置页面添加弹幕功能开关
- 调整数据刷新间隔和启动时更新信息的布局
- 在股票页面实现弹幕功能,根据设置开关控制是否显示弹幕
- 调整应用窗口高度比例
- 优化 OpenAI API 请求时的 URL 处理
2025-02-26 22:17:17 +08:00
ArvinLovegood
8a7e0140eb refactor(gui):调整应用窗口宽度比例
- 将应用窗口宽度从屏幕宽度的2/3 调整为 4/5
- 此修改旨在优化用户界面布局,提供更好的视觉体验
2025-02-25 22:12:27 +08:00
ArvinLovegood
797a35eaa5 feat(stock-data):添加屏幕分辨率适配,动态调整应用窗口大小
- 新增 GetRealTimeStockPriceInfo 函数,用于获取指定股票的实时价格和时间
- 优化爬虫配置,提高数据抓取效率
- 添加屏幕分辨率适配,动态调整应用窗口大小
- 修复部分股票代码格式问题,确保数据准确性
2025-02-25 22:07:33 +08:00
ArvinLovegood
1763435aa1 docs(README): 更新港股支持状态
- 在 README.md 文件中更新了港股支持的状态
- 添加说明,目前港股数据支持有延迟
2025-02-24 12:16:56 +08:00
ArvinLovegood
7952c1fceb feat(hk):更新港股数据支持并添加新功能
- 更新 README.md,添加 ETF 和美股支持计划
- 修改 stock.vue,增加弹幕相关图标和功能
- 更新 stock_data_api.go,添加股票价格时间信息
- 修改 stock_data_api_test.go,更新测试用例
2025-02-24 12:15:42 +08:00
ArvinLovegood
fbb8b00315 feat(app): 增加港股交易时间判断并更新相关功能
- 在 app.go 中添加 IsHKTradingTime 函数,用于判断当前时间是否在港股交易时间内
- 更新股票监控逻辑,使其在港股交易时间也能正常运行
- 在前端 stock 组件中添加股票代码标签,并根据股票代码动态显示货币符号
- 新增 app_test.go 文件,添加 IsHKTradingTime函数的单元测试
2025-02-24 10:12:23 +08:00
ArvinLovegood
2bf7d1e31f docs(README):添加弹幕功能更新日志
- 在 README.md 文件的更新日志部分添加了弹幕功能的记录
-弹幕功能使得盯盘不再孤单,增加了互动性
2025-02-24 09:07:45 +08:00
ArvinLovegood
cb2bc61c6f style(frontend):优化弹幕组件布局和交互
- 在 vue-danmaku 组件中添加 pointer-events: none 样式,确保弹幕不影响事件
-优化股票卡片的鼠标悬停和移出效果
2025-02-23 23:29:54 +08:00
ArvinLovegood
b3f23fc4db feat(frontend):添加弹幕功能并优化股票组件
- 在 stock.vue 中集成 vue3-danmaku 弹幕组件
- 实现 WebSocket 连接以接收实时弹幕消息
- 添加发送弹幕功能
- 优化股票搜索和显示逻辑
- 更新 App.vue 中的导入信息
- 在 package.json 中添加 vue3-danmaku 依赖
2025-02-23 22:02:20 +08:00
ArvinLovegood
67bd9e7996 Merge remote-tracking branch 'origin/master' 2025-02-23 21:58:18 +08:00
ArvinLovegood
4b9ae00452 feat(frontend):添加弹幕功能并优化股票组件
- 在 stock.vue 中集成 vue3-danmaku 弹幕组件
- 实现 WebSocket 连接以接收实时弹幕消息
- 添加发送弹幕功能
- 优化股票搜索和显示逻辑
- 更新 App.vue 中的导入信息
- 在 package.json 中添加 vue3-danmaku 依赖
2025-02-23 21:58:01 +08:00
sparkmemory
4baaefc8c5 fix(backend/data):修复香港股票数据解析并优化日期时间格式
- 在 ParseHKStockData函数中,增加 ";" 字符的分割,以解决数据格式问题- 优化日期和时间的解析,将日期格式从 "/" 改为 "-",时间格式进行相应调整
- 注释掉测试文件中的一个测试调用,可能是为了临时跳过该测试
2025-02-23 18:29:23 +08:00
ArvinLovegood
a6f17c632e feat(stock):添加香港股票数据支持
- 新增 StockInfoHK模型用于存储香港股票基本信息- 实现香港股票数据的爬取和解析功能
- 更新数据库初始化逻辑,支持香港股票数据导入
- 修改股票价格信息获取接口,支持香港股票
- 优化股票数据解析逻辑,适配香港股票数据格式
2025-02-22 21:47:05 +08:00
ArvinLovegood
4c249f0806 feat(wails.json): 更新软件备注信息
- 在 comments 字段中添加了更多支持的 AI 平台和模型信息
- 补充了软件的 GitHub 发行地址
2025-02-21 16:16:51 +08:00
ArvinLovegood
825014e370 fix(stock):修复AI重新检测库存时保留问题文本
- 移除了 aiReCheckStock函数中清除 question 字段的代码行
- 确保在重新检测库存时,之前的问题文本得以保留
2025-02-21 14:45:46 +08:00
ArvinLovegood
c91466a023 fix(stock):修复AI重新检测股票时保留问题文本的bug
- 在 aiReCheckStock函数中添加了清空 question 字段的逻辑
- 确保在重新检测股票时,不会保留上一次的问题文本
2025-02-21 14:38:01 +08:00
ArvinLovegood
92c61e4c26 fix(stock): 修复 AI 重新检测股票时保留问题文本的 bug
- 在 aiReCheckStock函数中添加了清空 question 字段的逻辑
- 确保在重新检测股票时,不会保留上一次的问题文本
2025-02-21 14:37:10 +08:00
ArvinLovegood
b34d2d8d76 docs(README): 更新 Gitee star 徽章链接
将 Gitee star 徽章的图片链接从自定义 URL 更改为官方提供的主题链接,以确保更好的兼容性和准确性。
2025-02-21 12:34:46 +08:00
ArvinLovegood
c287a82211 docs(README): 更新 Gitee star 徽章链接
-将 Gitee star 徽章的链接地址从 Hamm.cn 更改为 Gitee.com
- 优化 README 文档中的徽章展示
2025-02-21 12:08:40 +08:00
ArvinLovegood
1144ac34a7 docs(README): 添加 Gitee Star 徽章
在 README.md 文件中添加了 Gitee平台的 Star 徽章,以增加项目在不同平台上的可见性和互动性。
2025-02-21 12:07:00 +08:00
ArvinLovegood
1b66f0c0d8 docs(README): 添加项目徽章并优化格式
- 在 README.md 中添加了 GitHub Release 和星标徽章
- 优化了文档格式,包括调整图片链接格式和增加空行
2025-02-21 11:53:51 +08:00
ArvinLovegood
e597d3b484 docs: 更新README.md中的功能开发计划
在README.md中新增港股支持功能的状态为🚧,并调整了多轮对话和自定义AI分析提问模板的备注格式,使其更加清晰易读。
2025-02-20 18:05:27 +08:00
spark
cdc4b43925 refactor(data):调整KDays最小值为120天
- 在 openai_api.go 和 settings_api.go 文件中,将 KDays 的最小值从 30 天调整为 120 天
- 这个改动可能会影响到数据爬取和设置的相关功能
2025-02-19 20:49:50 +08:00
spark
0ff14fc01c refactor(data):优化数据处理和格式化
- 修改 OpenAI 消息内容格式,增加日期信息
- 重置股票指数和基本信息的 ID 为 0,以确保正确插入数据库
2025-02-19 20:37:56 +08:00
spark
5ccbbb6bb5 docs(frontend): 更新关于页面信息和使用说明
- 增加支持的 AI 平台和模型列表
- 添加 AI 分析股票结果的免责声明
- 修改联系方式备注说明,提高沟通效率
- 更新商业授权和定制开发的联系方式
- 优化页面布局和内容结构
2025-02-19 13:06:57 +08:00
spark
ec4a8659eb build(frontend):更新项目依赖并添加新功能支持:分析结果导出word文件
- 添加 @types/file-saver、@vavt/cm-extension、@vavt/v3-extension 和 file-saver依赖
- 更新 md-editor-v3 依赖至 5.2.3 版本
- 添加 html-docx-js-typescript 依赖
2025-02-19 12:23:55 +08:00
spark
34ac6755a9 refactor(backend):重构数据处理和前端AI分析结果展示
- 新增 Resp 结构体用于统一响应格式- 优化 OpenAI API 流数据处理逻辑,解析并展示具体错误信息
- 更新前端 stock组件,改进 AI 分析结果的接收和展示
- 调整代码格式,提高可读性
2025-02-18 14:19:40 +08:00
spark
e21ba1b800 feat(frontend/backend):添加日K线数据天数设置功能
- 在前端设置页面添加日 K 线数据天数配置选项
- 在后端 OpenAI 配置中添加 KDays 字段
- 调整股票数据分析时的历史数据时间范围
2025-02-18 12:32:34 +08:00
spark
17a234f679 修复敏感词问题导致deepseek无法分析 2025-02-17 21:53:50 +08:00
spark
d504dc6d13 docs(frontend): 更新长期技术支持描述
- 在 about.vue 和 README.md 中将"长期技术支持(不限次数,新功能优先体验)"修改为"长期技术支持(不限次数,新功能优先体验等)"
- 此修改旨在更准确地描述长期技术支持的内容,增加"等"字暗示可能还有其他权益
2025-02-17 17:57:44 +08:00
spark
0b749d1699 docs(frontend): 更新技术支持方式和费用
- 移除了邮件支持方式
- 更新了长期技术支持的描述和费用
- 统一了前端组件和 README 中的技术支持信息
2025-02-17 17:55:49 +08:00
spark
4e9a24c8f2 feat(README): 更新项目功能描述
- 在 README.md 中增加了对市场整体和个股情绪分析、K线技术指标分析等功能的描述
2025-02-17 17:39:19 +08:00
spark
c81b1a730d feat(data):添加tushare数据接口并优化股票代码转换功能(设置好提问模板后可进行K线分析功能)
- 新增 TushareApi 结构体和 GetDaily 方法,用于获取 A 股日线行情数据
- 在 openai_api.go 中添加获取股票日 K线数据的协程
- 在 utils.go 中添加股票代码与 tushare 代码相互转换的函数
- 更新相关测试文件以支持新功能
2025-02-17 17:33:17 +08:00
spark
8d3cd7b151 feat:完成多轮对话功能开发
- 将多轮对话功能的状态从 🚧 更新为 
- 在更新日志中添加 2025.02.16 版本信息,说明 AI 分析后可继续对话提问
- 新增 v2025.2.16.1-alpha 版本号
2025-02-17 12:13:16 +08:00
spark
5ee1ae4a32 feat(frontend):优化AI聊天功能并添加新功能
- 新增用户自定义问题输入功能
- 优化 AI回答的展示逻辑
- 添加错误处理和提示
- 更新后端接口以支持新功能
2025-02-16 21:56:07 +08:00
spark
dab51f7a70 feat(backend):添加持仓成本价(costPrice)变量到用户提问问题模板
- 在 openai_api.go 文件中,增加了对持仓成本价的处理
- 通过查询数据库获取股票的持仓成本价,并加入到替换模板中
- 更新了问题模板的替换逻辑,支持新的成本价变量
2025-02-15 15:30:46 +08:00
spark
a20d4e721d feat(data):优化数据处理和模型结果展示(ps:今天白天太忙了,更新内容较少)
- 修改文本处理方法,提高消息内容的可读性
- 在 AIResponseResult模型中添加 modelName 字段
- 更新前端组件,展示模型名称信息
- 优化数据库查询,提高响应速度
2025-02-14 22:39:57 +08:00
spark
f4da21d645 feat(backend):添加股市通资讯爬取功能
- 新增 SearchGuShiTongStockInfo函数,用于爬取百度股市通的股票资讯
- 修改 OpenAI_API 函数,增加股市通资讯的爬取
- 添加 RemoveAllNonDigitChar 函数,用于去除所有非数字字符
2025-02-13 17:56:22 +08:00
spark
fc37440f6b docs(frontend):更新关于页面中的技术支持说明
- 在 about.vue 文件中,增加了向开源社区请求帮助的建议
- 修改了对一对一技术支持的描述,强调在确实需要时再进行赞助和联系
2025-02-13 14:59:40 +08:00
spark
d7b47a7010 docs(frontend): 更新关于页面的联系方式说明
- 在 about.vue 文件中,增加了对添加微信或 QQ 时的备注说明
- 添加了技术支持的链接,提高了可点击性
- 优化了页面布局,增加了视觉区分度
2025-02-13 14:56:37 +08:00
spark
85d71ae58e docs: 更新 issue 模板 2025-02-13 14:40:59 +08:00
spark
23dc25f642 docs: 更新 Bug 报告模板为中文版本
- 将 Bug 报告模板的英文内容翻译为中文
- 优化模板结构,增加更多详细分类和说明
- 添加复现步骤、频率、错误日志等新字段
- 引入截图或视频上传建议
- 增加可能的原因分析和补充说明部分
2025-02-13 14:34:13 +08:00
spark
467bbd8923 refactor(data):重构数据爬取功能
- 新增 CrawlerApi 结构体和相关方法,实现通用的爬虫功能
- 优化了 openai_api 和 stock_data_api 中的爬虫逻辑
- 添加了 RemoveAllBlankChar函数,用于移除字符串中的空白字符
- 更新了前端 stock组件中的警告提示
2025-02-13 14:16:56 +08:00
spark
6be5c0fa05 refactor(stock_data):优化股票信息搜索功能
- 修改 SearchStockInfo 函数,增加对不同消息类型的处理
- 更新页面等待逻辑,根据消息类型选择不同的选择器
- 调整测试函数,增加时间参数
2025-02-12 22:48:17 +08:00
spark
4fac915778 refactor(stock_data):优化股票信息搜索功能
- 修改 SearchStockInfo 函数,增加对不同消息类型的处理
- 更新页面等待逻辑,根据消息类型选择不同的选择器
- 调整测试函数,增加时间参数
2025-02-12 21:52:27 +08:00
spark
d27bcbd334 feat(backend):添加资讯采集超时设置并优化相关功能
- 在 OpenAi 结构中添加 CrawlTimeOut 字段,用于设置资讯采集超时时间
- 修改相关函数以支持新的超时设置,包括 GetFinancialReports、GetTelegraphList、GetTopNewsList等
- 在前端设置页面添加 Crawler Timeout 设置项
- 优化浏览器检查逻辑,优先检查 Chrome 浏览器
2025-02-12 17:03:25 +08:00
spark
d46872ffbd docs(README):可配置的提问模板
- 更新可配置提问模板的版本号为 v2025.2.12.5-alpha
- 调整更新日志和功能开发计划的格式
- 更新功能开发计划中的状态标识
2025-02-12 15:23:28 +08:00
spark
29da37739d docs(README): 可配置的提问模板,更新功能开发计划和版本发布说明
- 更新可配置提问模板的版本号为 v2025.2.12.5-alpha
- 调整更新日志和功能开发计划的格式
- 更新功能开发计划中的状态标识
2025-02-12 15:14:44 +08:00
spark
d7584bc4de docs(README): 更新功能开发计划和发布新版本
- 新增可配置的提问模板功能
- 更新功能开发计划,将自定义AI分析提问模板状态标记为已完成
- 调整不再强制依赖Chrome浏览器状态为已完成
- 优化README结构,增加更新日志部分
2025-02-12 14:56:27 +08:00
spark
1f78cc3589 feat(frontend/backend):增加自定义用户提问模板功能
- 在 Settings 模型中添加 questionTemplate 字段
- 在 OpenAi 结构体中添加 QuestionTemplate 字段
- 更新前端设置组件,增加用户 prompt 配置选项
- 修改后端 API调用,支持使用自定义用户 prompt
2025-02-12 14:47:50 +08:00
spark
a3b718c149 refactor(frontend):移除AI分析结果时间的判断逻辑
移除了 stock.vue 组件中 AI 分析结果下方提示文字的时间判断逻辑。现在,无论是否有分析时间,提示文字将始终显示,以确保用户在任何情况下都能看到投资风险提示。
2025-02-12 12:57:02 +08:00
spark
37e63538e2 refactor(data):添加chromedriver路径日志输出
- 在 GetFinancialReports、SearchStockPriceInfo 和 SearchStockInfo 函数中添加了 chromedriver 路径的日志输出
- 有助于调试和验证 chromedriver 的正确路径,确保自动化任务顺利进行
2025-02-12 12:53:15 +08:00
spark
70ee9df22a fix(data):优化Edge浏览器调用逻辑修复AI分析使用BUG
- 在 Windows 系统上动态检查 Edge 浏览器安装情况
- 根据系统环境选择合适的 Edge 可执行文件路径
- 优化了 openai_api 和 stock_data_api 中的 Edge 调用逻辑
2025-02-12 12:17:51 +08:00
spark
b764c978f1 docs(README): 添加关于版权和技术支持的声明
- 增加了本软件基于开源技术构建的说明
- 提供了技术支持的联系方式和赞助费用
- 添加了长期技术支持的选项和费用说明
2025-02-12 11:41:06 +08:00
spark
fc8dbb919c docs(README): 添加关于版权和技术支持的声明
- 增加了本软件基于开源技术构建的说明
- 提供了技术支持的联系方式和赞助费用
- 添加了长期技术支持的选项和费用说明
2025-02-12 11:26:18 +08:00
spark
02e3d1df11 feat(frontend):添加关于版权和技术支持申明
- 在关于页面中增加了关于版权和技术支持的申明内容
- 添加了技术支持的联系方式和赞助费用说明
-优化了页面布局,移除了不必要的组件嵌套
2025-02-12 11:14:33 +08:00
spark
e074ab2c39 feat(frontend): 添加支付宝和微信支付二维码
- 在 about.vue 中添加了支付宝和微信支付的二维码图片
- 在 VersionInfo 模型中增加了 Alipay 和 Wxpay 字段
- 更新了后端和前端的相关代码,支持支付二维码的获取和显示
- 在 stock.vue 中添加了 AI 分析结果的免责声明
2025-02-12 10:51:24 +08:00
spark
e2e5a063e7 docs(README): 添加 AnythingLLM 到大模型平台列表
在 README.md 文件中的大模型平台对比表格中添加了 AnythingLLM。
2025-02-12 10:17:04 +08:00
spark
c4c2bea73d docs: 更新 README.md 中的项目描述
- 移除了自选股行情实时监控的描述
-简化了项目介绍,突出了基于 Wails 和 NaiveUI 构建的 AI 赋能股票分析工具的核心信息
2025-02-12 09:51:29 +08:00
spark
bfd9515387 docs: 更新 README.md 标题
在 README.md 文件中,为项目名称 "go-stock" 添加了前缀,使其更加显眼。这一修改旨在提高项目名称的可见性,以便于访客快速识别项目的主题。
2025-02-12 09:50:20 +08:00
spark
7029d75790 docs(README): 更新功能开发表格中的描述
- 将"自定义提问模板"改为"自定义AI分析提问模板"
- 将"可配置的提问"改为"可配置的提问模板"
2025-02-12 09:41:39 +08:00
spark
13d1c75b76 feat: 不再强制安装Chrome浏览器
- 默认使用Edge浏览器抓取新闻资讯
- 提高了系统的灵活性和用户体验
2025-02-12 09:40:34 +08:00
spark
aaf53f651a docs: 修改功能开发标题
- 将"计划开发"更改为"功能开发",以更准确地描述功能列表的性质
- 优化文档结构,提高可读性和清晰度
2025-02-12 09:38:43 +08:00
spark
dad9ece712 docs(README): 添加计划开发功能列表
- 在 README.md 中新增了"计划开发"章节
- 增加了自定义提问模板和多轮对话两个即将开发的功能
- 为每个功能添加了状态和备注信息
2025-02-12 09:37:59 +08:00
spark
6e17e89961 docs(README): 添加计划开发功能列表
- 在 README.md 中新增了"计划开发"章节
- 增加了自定义提问模板和多轮对话两个即将开发的功能
- 为每个功能添加了状态和备注信息
2025-02-12 09:36:09 +08:00
spark
fae5a5fb6a docs(README): 更新项目文档和截图
- 添加项目简介、大模型支持情况等新内容
- 更新功能截图
- 调整文档结构,优化标题层级- 移除部分冗余信息,提高可读性
2025-02-12 09:29:39 +08:00
spark
96f2898111 docs: 更新 README 状态部分,添加 Star History 图表
- 在 README.md 文件的"状态"部分,添加了 Star History 图表的嵌入代码
- 该图表展示了项目星星随时间的增长情况,为潜在贡献者提供了项目热度的可视化信息
2025-02-12 08:50:24 +08:00
spark
e24965393b feat(browser):使用Edge替代Chrome执行AI分析时的依赖
- 新增 checkEdgeOnWindows 函数以检查 Edge 浏览器安装情况
- 修改 AI 分析相关功能,使用 Edge 浏览器代替 Chrome
- 更新相关日志和错误处理
2025-02-11 21:33:11 +08:00
spark
7e5d135483 feat(stock):保存分析结果为Markdown文件
- 新增 saveAsMarkdown 函数,用于将分析结果保存为 Markdown 文件
- 更新 saveAsImage 函数,添加股票名称和代码到文件名
- 在股票分析结果页面添加保存为 Markdown 文件的按钮
2025-02-11 17:59:50 +08:00
spark
c8827da35a feat(frontend):添加AI分析结果保存和复制功能
- 在 stock.vue 组件中添加了保存分析结果为图片和复制到剪切板的功能
- 引入了 html2canvas 库用于截图
- 优化了 AI 分析结果弹窗的布局
2025-02-11 17:53:06 +08:00
spark
268bc8b1f6 docs(README):添加Tushare数据社区注册链接
- 在 README.md 中添加了 Tushare大数据开放社区的注册链接- Tushare 提供免费的各类金融数据,助力行业和量化研究
2025-02-11 17:25:23 +08:00
spark
2869b37053 docs(README):添加Tushare数据社区注册链接
- 在 README.md 中添加了 Tushare大数据开放社区的注册链接- Tushare 提供免费的各类金融数据,助力行业和量化研究
2025-02-11 17:23:18 +08:00
spark
b237341fda docs: 添加项目状态图表
- 在 README.md 中插入 Repobeats analytics 图像,展示项目活动状态
2025-02-11 15:23:54 +08:00
spark
957de8ad8b feat(backend):添加获取新闻资讯功能
- 新增 GetTopNewsList 函数,用于获取新闻资讯
- 在处理用户消息时,添加获取新闻资讯的逻辑
- 当获取新闻资讯失败时,发送警告消息
2025-02-11 15:10:29 +08:00
spark
e622b7d86e refactor(frontend):注释掉错误消息弹窗
- 在 settings.vue 文件中注释掉了错误消息弹窗的代码行
-这个修改可能会影响错误处理的用户界面展示
2025-02-11 13:56:39 +08:00
spark
95f9f1840f refactor(data):重构OpenAi结构体并添加上下文对象
- 在 OpenAi 结构体中添加 ctx 字段,用于传递上下文对象
- 更新 NewDeepSeekOpenAi 函数签名,现在需要传入 context.Context 参数
- 在获取股票信息失败时,除了在控制台输出错误信息外,还通过 runtime.EventsEmit 发送警告消息到前端
- 优化错误信息的显示格式,添加警告图标
2025-02-11 13:34:14 +08:00
spark
267f6f638f refactor(data):重构OpenAi结构体并添加上下文对象
- 在 OpenAi 结构体中添加 ctx 字段,用于传递上下文对象
- 更新 NewDeepSeekOpenAi 函数签名,现在需要传入 context.Context 参数
- 在获取股票信息失败时,除了在控制台输出错误信息外,还通过 runtime.EventsEmit 发送警告消息到前端
- 优化错误信息的显示格式,添加警告图标
2025-02-11 12:52:32 +08:00
spark
b459abb35d feat(data):为数据获取失败时添加错误反馈并设置超时
- 在获取股票价格、财报、市场资讯、股票资讯和电报资讯失败时,向用户发送错误信息
- 为 GetFinancialReports、SearchStockPriceInfo 和 SearchStockInfo 函数添加 30 秒超时设置
2025-02-11 12:29:49 +08:00
spark
d79bdc8bc1 feat(frontend):更新页面标题和优化错误处理
- 更新页面标题为 "go-stock:AI赋能股票分析"
- 改进全局错误处理,增加错误信息的控制台输出
- 优化设置组件中的错误提示和表单重置逻辑
2025-02-11 09:34:40 +08:00
spark
863e88c579 refactor(frontend):优化错误捕获和日志记录功能
- 修改 App.vue、settings.vue 和 stock.vue 中的 window.onerror 函数,增加页面标识和友好的错误提示
- 优化 openai_api.go 中的错误捕获,增加详细的日志记录
- 统一错误消息参数,提高错误信息的准确性和可读性
2025-02-10 18:06:49 +08:00
spark
853f6b180e refactor(frontend):优化错误捕获和日志记录功能
- 修改 App.vue、settings.vue 和 stock.vue 中的 window.onerror 函数,增加页面标识和友好的错误提示
- 优化 openai_api.go 中的错误捕获,增加详细的日志记录
- 统一错误消息参数,提高错误信息的准确性和可读性
2025-02-10 18:06:31 +08:00
spark
b4c55ce233 feat(frontend):增加前端错误捕获和后端panic处理
- 在前端 App.vue、settings.vue 和 stock.vue 中添加 window.onerror 事件处理器,捕获前端错误并发送给后端
- 在后端 app.go 和 openai_api.go 中添加 panic 处理逻辑,捕获并记录 panic错误
- 在 main.go 中添加 PanicHandler 函数,用于捕获和处理全局 panic
2025-02-10 17:46:42 +08:00
spark
22111411c3 ci:更新GitHubActions工作流
- 修改了工作流中的构建步骤名称,从 "Build wails" 改为 "Build wails x go-stock"
- 这个更改更准确地描述了构建过程,即使用 wails 构建 go-stock 项目
2025-02-10 14:12:38 +08:00
spark
ddfc7c1216 feat(app):添加谷歌浏览器检查并发送警告消息
- 在应用启动时检查谷歌浏览器是否安装
- 如果未安装,发送警告消息提醒用户
- 新增 checkChromeOnWindows 函数用于检查浏览器
- 在前端添加警告消息的事件监听
2025-02-10 13:59:23 +08:00
spark
908086c0c0 feat(app):添加谷歌浏览器检查并发送警告消息
- 在应用启动时检查谷歌浏览器是否安装
- 如果未安装,发送警告消息提醒用户
- 新增 checkChromeOnWindows 函数用于检查浏览器
- 在前端添加警告消息的事件监听
2025-02-10 13:50:51 +08:00
spark
2c0e2ec698 build(frontend): 升级Vite 版本
- 将 Vite 版本从 5.4.6 升级到 5.4.12- 更新 package.json 和 package-lock.json 中的 Vite 依赖
- 更新 package.json.md5 校验值
2025-02-10 13:25:12 +08:00
spark
cb4690bf88 docs(README): 添加软件下载安装说明
- 在 README.md 中新增了下载安装部分
- 提供了安装版和绿色版两种版本的下载链接
- 优化了文档结构,方便用户快速找到下载信息
2025-02-10 13:16:35 +08:00
spark
100fb3e2a9 docs(README): 添加软件下载安装说明
- 在 README.md 中新增了下载安装部分
- 提供了安装版和绿色版两种版本的下载链接
- 优化了文档结构,方便用户快速找到下载信息
2025-02-10 13:14:34 +08:00
Lovegood
1bb13866bc Update README.md 2025-02-10 13:03:08 +08:00
spark
05aaf82849 docs: 更新 README 中的 star 请求样式
- 在 README.md 文件中,将"请给我一个 star"的部分添加了颜色样式
- 使用 span 标签为文本添加蓝色背景和红色"star"字- 优化了视觉效果,使请求更醒目
2025-02-10 13:00:55 +08:00
spark
255a214554 docs(README):添加项目星标请求
在 README.md 文件开头加入了一行文本,请求感兴趣的用户给项目星标。这有助于增加项目的 visibility 和吸引更多的贡献者。
2025-02-10 12:50:58 +08:00
spark
5d0de949a8 docs(README):添加项目星标请求
在 README.md 文件开头加入了一行文本,请求感兴趣的用户给项目星标。这有助于增加项目的 visibility 和吸引更多的贡献者。
2025-02-10 12:49:38 +08:00
Lovegood
03229fa46b Create SECURITY.md 2025-02-10 12:29:51 +08:00
spark
b4cdd2d28b docs:更新行为准则的中文翻译版本
- 将中文翻译版本从代码文件中移除
- 在文件顶部添加完整的中文翻译内容
- 更新文件结构,使中文版本更加突出和易于阅读
2025-02-10 12:17:56 +08:00
spark
ef838af18b feat(frontend):设zhi默认prompt
- 在 settings.vue 中添加了 AI 投资顾问的默认角色设定
- 设定了 AI 投资顾问的核心能力和服务范围
- 定义了 AI 投资顾问的交互风格和表达方式- 优化了 prompt 的默认值,确保 AI 生成的内容符合预期
2025-02-10 12:16:54 +08:00
Lovegood
62defa1ebc Update LICENSE 2025-02-10 12:12:26 +08:00
Lovegood
3dd9790015 Create CONTRIBUTING.md 2025-02-10 12:07:35 +08:00
Lovegood
c25304d28b Create CODE_OF_CONDUCT.md 2025-02-10 12:00:54 +08:00
spark
222ba03841 docs(README):更新设置界面截图
- 将 README.md 中的 img_12.png 图片引用从旧路径修改为新路径- 旧路径: build/screenshot/img_12.png
- 新路径: build/screenshot/img_4.png
2025-02-10 10:01:23 +08:00
spark
3f73a5a521 feat(frontend):添加设置导出导入功能
- 在 App.d.ts 中添加 ExportConfig 函数声明
- 在 app.go 中实现 ExportConfig 方法,用于导出配置文件
- 在 App.js 中添加 ExportConfig 函数的 JavaScript 调用接口
- 在 settings.vue 中添加导出和导入配置的功能按钮,并实现相关逻辑
- 在 settings_api.go 中添加 Export 方法,用于生成配置文件的 JSON 字符串
2025-02-10 09:47:10 +08:00
spark
3a3e0b0543 feat(frontend):添加设置导出导入功能
- 在 App.d.ts 中添加 ExportConfig 函数声明
- 在 app.go 中实现 ExportConfig 方法,用于导出配置文件
- 在 App.js 中添加 ExportConfig 函数的 JavaScript 调用接口
- 在 settings.vue 中添加导出和导入配置的功能按钮,并实现相关逻辑
- 在 settings_api.go 中添加 Export 方法,用于生成配置文件的 JSON 字符串
2025-02-10 09:45:51 +08:00
spark
0006501cc8 fix(data):优化数据获取流程并添加错误日志
- 在获取股票价格、财报、市场资讯等数据时,增加了空值判断并记录错误日志
-优化了数据获取流程,提高了代码的健壮性和可维护性- 在 chromedp 上下文中添加了日志记录,便于调试和排查问题
2025-02-09 20:58:45 +08:00
spark
c5fbe5fdae Merge branch 'master' of https://github.com/ArvinLovegood/go-stock 2025-02-09 20:18:42 +08:00
spark
66d85cf0a2 fix(backend):修复chromedp未取消导致的资源泄漏问题
- 在 openai_api.go 和 stock_data_api.go 中添加了对 chromedp.Cancel 的调用
- 确保在请求完成后正确取消 chromedp 的执行上下文,释放资源
2025-02-09 20:18:05 +08:00
spark
24145894b6 refactor(app):优化GetStockInfos函数,避免闪退
- 移除错误处理,因为调用方可能不需要错误信息
- 调整变量初始化顺序,提高代码可读性
- 简化错误处理逻辑,忽略错误并返回空值
2025-02-09 19:20:46 +08:00
Lovegood
75f680a298 Update issue templates 2025-02-09 17:16:59 +08:00
spark
2658f207dc feat(frontend):使用内嵌应用图标
- 使用内嵌应用图标替换URL图标
- 添加 GetVersionInfo 函数调用,用于获取版本信息
2025-02-09 16:26:20 +08:00
spark
626f99f0d1 refactor(frontend):重构关于页面布局
- 使用 n-divider组件替代 h1 标题,提高页面美观度
- 移除多余的 n-card 嵌套,简化页面结构
- 注释掉多余的 h1 标题,优化代码可读性
2025-02-09 16:17:12 +08:00
spark
4d541e81a2 feat(frontend):添加鸣谢部分
- 增加了捐赠者、开发者和开源项目的鸣谢列表
- 优化了关于页面的布局,使鸣谢内容更加突出
- 添加了外部链接,方便用户访问相关开源项目
2025-02-09 16:04:45 +08:00
spark
6dfe3fd135 feat(frontend):添加鸣谢部分
- 增加了捐赠者、开发者和开源项目的鸣谢列表
- 优化了关于页面的布局,使鸣谢内容更加突出
- 添加了外部链接,方便用户访问相关开源项目
2025-02-09 16:01:47 +08:00
spark
915e12eab3 feat(frontend):丰富关于页面内容并优化布局
- 在 GitHub 链接旁边添加 Issues 和 Releases 链接
- 在邮箱下方添加 QQ 和微信联系方式- 使用 n-divider 组件进行垂直分割,提高可读性
2025-02-09 15:40:04 +08:00
spark
bcfcbfeef0 fix(stock):优化股票关注功能
- 增加股票代码有效性验证
- 改进关注失败时的错误处理和用户提示
- 修复可能的 nil pointer dereference 问题
2025-02-09 15:29:04 +08:00
spark
1dc731de1e refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:50:13 +08:00
spark
a580f9254a refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:44:39 +08:00
spark
9b080bbb45 refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:26:10 +08:00
spark
86183f4585 build:更新Wails构建动作版本 2025-02-08 16:48:12 +08:00
spark
97ab29259a ci:精简 commit message 输出
- 修改了获取 commit message 的 git 命令,仅保留 commit 主题行
-移除了不必要的信息,如作者和日期
- 优化了输出格式,提高了可读性
2025-02-08 16:30:54 +08:00
spark
91f3e66239 ci: 更新获取 commit message 的命令
- 修复了获取 commit message 时的语法错误
- 使用 PowerShell 兼容的命令格式
2025-02-08 16:06:59 +08:00
Lovegood
713b25d2db Update main.yml 2025-02-08 15:46:58 +08:00
spark
d0b65e7063 ci: 更新获取 commit message 的命令
- 修复了获取 commit message 时的语法错误
- 使用 PowerShell 兼容的命令格式
2025-02-08 15:34:40 +08:00
spark
f062306158 build: 更新 Wails 构建动作版本
- 将 ArvinLovegood/wails-build-action 版本从 v2.5 升级到 v2.6
- 保持其他配置不变,仅更新动作版本
2025-02-08 15:25:35 +08:00
spark
ae7b617e83 feat(frontend): 添加关于软件页面并实现版本信息动态获取
- 新增 about.vue 组件,包含软件介绍、更新说明和作者信息
- 添加 GetVersionInfo 函数,用于获取版本信息
- 在 App.vue 中添加关于软件的菜单项
- 在 router.js 中添加关于软件的路由
- 优化页面布局和样式
2025-02-08 15:05:52 +08:00
spark
1035f2a800 feat(frontend): 添加关于软件页面
- 在 App.vue 中添加关于软件的菜单项
- 在 router.js 中添加关于软件的路由- 新增 about.vue 组件,包含软件介绍和作者信息
2025-02-08 12:20:40 +08:00
spark
cb28b18541 docs(README): 更新 AI 分析股票功能截图
- 将 AI 分析股票功能的截图从 img_10.png 修改为 img.png
- 更新 README.md 中的相关图片引用
2025-02-08 11:39:39 +08:00
spark
9d42eb2729 docs(README): 更新项目介绍和赞助信息 2025-02-08 11:24:56 +08:00
spark
7b93d4d8ca feat(data): 添加 AIResponseResult模型并实现相关功能
感谢 @gnim2600 的建议!

- 新增 AIResponseResult 模型用于保存 AI 分析结果
- 实现 SaveAIResponseResult 和 GetAIResponseResult 函数
- 在前端添加 AI 分析功能,包括保存和获取分析结果
-优化 AI 分析界面,增加分析时间显示和再次分析按钮
2025-02-08 11:13:17 +08:00
spark
3e13ef007b feat(openai): 添加 OpenAI API 超时设置并调整相关功能
感谢@gnim2600 @XXXiaohuayanGGG 两位提供的帮助和建议
- 在前端和后端添加 OpenAI API 超时设置选项
- 更新 AI 诊断股票功能,支持自定义超时时间
- 优化设置界面布局,提高用户体验
- 为 AI 分析结果添加居中显示样式
2025-02-08 09:14:09 +08:00
spark
6ff1b68f1b fix:修复 GitHub 时间转换错误
- 移除了 getTimezoneOffset() * 60 * 1000 的计算
-现在直接使用 utcDate.getTime() 获取时间戳
2025-02-07 11:21:05 +08:00
spark
a6547db195 docs(README): 添加版本信息提示截图
- 在 README.md 中新增了版本信息提示部分
- 添加了对应的截图 img_11.png
2025-02-07 11:03:01 +08:00
spark
567414a136 feat(update): 增加新版本详细信息和发布时间
- 获取并显示新版本的 Tag 和 Commit 信息
- 将 UTC 时间转换为本地时间并显示
- 在通知中添加新版本详细信息和发布时间
- 优化股票卡片样式,增加鼠标悬停效果
2025-02-07 10:49:55 +08:00
Lovegood
34dc38a95f Merge pull request #3 from 2lovecode/feature-support-macos
feat(macos):support macos
2025-02-06 20:12:38 +08:00
2lovecode
6d2ab3ef41 feat(macos):support macos 2025-02-06 18:03:06 +08:00
spark
e55506705e feat(update): 添加软件更新检查功能
- 在应用启动时检查 GitHub 上的最新版本
- 如果发现新版本,通过通知提示用户更新
- 新增 GitHubReleaseVersion模型用于解析版本信息
- 在前端添加更新通知的展示逻辑
2025-02-06 16:19:11 +08:00
spark
322e87efbd ci:为 GitHub Actions 添加 build-tags 参数
在 GitHub Actions 的构建配置中添加了 build-tags 参数,使其等于当前的引用名称(github.ref_name)。这允许我们在构建过程中使用特定的标记。

- 修改了 .github/workflows/main.yml 文件- 在 build-platform 部分添加了 build-tags 参数
- 参数值设置为当前引用名称,增加了构建的灵活性和可追溯性
2025-02-06 15:01:36 +08:00
spark
1628381295 feat(app): 添加版本信息,为更新推送做准备
- 在应用启动时打印版本号
2025-02-06 14:53:07 +08:00
spark
8afc26badb test:移除雪球和硅流 API 调用相关代码 2025-02-05 16:44:00 +08:00
spark
d5db2ef879 feat(backend): 添加获取财务报告功能并优化聊天流
- 新增 GetFinancialReports 函数,用于抓取股票财务报告信息
- 优化 NewChatStream 函数,增加财务报告信息到聊天流中
- 更新测试用例,使用北京文化(sz000802)作为示例股票- 添加 TestGetFinancialReports 和 TestXUEQIU 测试函数
2025-02-05 16:25:24 +08:00
spark
509cd2dbca refactor(backend): 调整 openai_api.go 中的资源关闭逻辑
- 将 resp.RawBody().Close() 调用移动到 if err != nil块之后
- 确保在发生错误时也能正确关闭网络连接
- 优化了代码结构,提高了资源管理的可靠性
2025-02-04 20:02:21 +08:00
spark
3de2ad3cdc refactor(backend): 重构 OpenAI 和股票数据 API
-优化了 OpenAI API 的调用逻辑,提高了错误处理和数据处理的能力
- 改进了股票数据 API 的数据抓取和处理方式
- 移除了测试代码中冗余的部分,提高了代码可读性和维护性
2025-02-04 19:45:22 +08:00
spark
b00bddcdec refactor(stock-data): 重构股票数据获取逻辑
- 移除了不必要的并发请求,简化了代码结构
- 新增 FetchPrice 函数,用于获取股票价格信息
- 优化 SearchStockInfo 函数,提高了搜索效率和准确性
- 新增 SearchStockInfoByCode 函数,用于根据股票代码获取相关信息- 修复了一些潜在的错误和性能问题
2025-02-04 18:12:08 +08:00
spark
64b37b687c refactor(data): 优化 OpenAI API 客户端配置并改进流数据处理
- 将请求超时时间从 30秒增加到 60 秒
- 修正流数据的前缀检查,从 "chat data: " 改为 "data: "- 增加对 reasoning_content 的处理逻辑
- 优化数据处理流程,提高错误处理能力
2025-02-04 15:12:15 +08:00
spark
e81319bb4f docs(README): 添加赞助信息在 README.md 中添加了赞助信息部分,提供了支付宝和微信支付的二维码图片链接,鼓励对项目有帮助的用户进行赞助。 2025-02-04 07:31:44 +08:00
spark
7bc219d1a5 refactor(frontend): 优化 OpenAI 设置界面文案
- 将"自定义Prompt"标签修改为"自定义系统Prompt"
- 更新输入框占位符为"请输入系统prompt"
2025-02-03 22:03:16 +08:00
spark
0f2f58e6b8 docs: 更新 README 中的设置截图
- 将 README.md 中的 img_11.png 替换为 img_12.png
- 优化设置界面的视觉效果
2025-02-03 21:53:31 +08:00
spark
2dc0b95b45 docs: 更新 README 中的设置截图
- 将 README.md 中的 img_11.png 替换为 img_12.png
- 优化设置界面的视觉效果
2025-02-03 14:01:20 +08:00
spark
869eced99e refactor(backend): 优化 API 客户端配置并调整日志输出
- 为 OpenAI API 客户端添加重试次数和超时设置
- 修改 OpenAI API 客户端初始化,设置基础 URL
- 优化 OpenAI API 响应数据的处理逻辑
- 为 stock_data API 客户端添加重试次数设置
- 在 stock_data API 中添加日志和错误处理
2025-02-03 13:50:13 +08:00
spark
f5aa70bf61 feat(main): 调整窗口最大宽度和高度并启用默认上下文菜单
- 将窗口最大宽度从 1280调整为 1920
- 启用默认上下文菜单
2025-02-01 13:18:43 +08:00
spark
71289d1408 feat(openai): 添加自定义 prompt 功能
- 更新前端设置组件,增加自定义 prompt 输入框
- 更新后端设置 API,支持保存和读取 prompt 配置
2025-02-01 11:32:38 +08:00
spark
f6297d224c refactor(frontend): 优化AI股票分析用户体验
- 在股票检测过程中添加 loading 状态
2025-01-31 16:26:36 +08:00
spark
0bfa50e2b6 feat(frontend): 优化 AI 分析功能
- 添加 loading 状态和DONE消息处理
- 改进消息提示和销毁逻辑
- 优化 AI 分析结果的展示
- 调整 API测试日志输出
2025-01-31 15:50:47 +08:00
spark
0d182b923f build: 更新 wails 构建动作
- 将 dAppServer/wails-build-action 替换为 ArvinLovegood/wails-build-action
- 指定使用2.3 版本的 wails 构建动作
2025-01-31 13:46:29 +08:00
spark
7f19d00a23 build: 更新 wails 构建动作
- 将 dAppServer/wails-build-action 替换为 ArvinLovegood/wails-build-action
- 指定使用2.3 版本的 wails 构建动作
2025-01-31 13:44:49 +08:00
spark
f514a0083d build: 更新 wails 构建动作引用
- 将 dAppServer/wails-build-action@latest替换为 dAppServer/wails-build-action
- 此修改旨在解决特定版本冲突问题
2025-01-31 13:40:51 +08:00
spark
7fbe178c78 build: 更新 Wails 构建动作版本
- 将 dAppServer/wails-build-action 版本从 v2.2 修改为 latest
- 此更改确保使用最新版本的 Wails 构建动作,可能包含未记录的最新功能和修复
2025-01-31 13:38:13 +08:00
spark
40fe30ce2f refactor(data): 重构股票数据获取方法
- 优化了股票价格信息的获取流程,增加了对关键元素的等待和检查
- 改进了搜索结果页面的处理,使用更准确的选择器进行等待
- 删除了不必要的测试函数,整合了相关的测试用例
2025-01-31 13:02:04 +08:00
spark
1751be729b feat(frontend): 添加 Tushare 接口 token 配置功能
- 在前端设置页面增加 Tushare api token 输入框
- 在后端 Settings 结构体中添加 TushareToken 字段- 更新相关 API 调用,使用配置的 TushareToken
2025-01-26 21:41:48 +08:00
spark
d82ace220a ci: 注释掉 Linux 构建
- 不在支持Linux平台,专注Windows平台
- 在 GitHub Actions 配置文件中注释掉了 Linux构建相关配置
- 此修改将阻止在 Ubuntu 系统上进行 go-stock-linux-amd64 的构建
2025-01-26 11:53:00 +08:00
spark
9c51ecde2f ci: 更新作者邮箱地址
- 将作者邮箱从 "wzl@huazx.cn" 修改为 "sparkmemory@163.com"
2025-01-26 11:50:58 +08:00
spark
d3cf202c88 feat(frontend): 增加 AI 赋能股票分析功能
- 在 package.json 中添加 AI 赋能股票分析相关关键词
- 更新 settings.vue 中的 openAI 配置项,优化输入框类型和样式
- 在 wails.json 中添加 AI 赋能分析股票的说明
2025-01-26 11:44:50 +08:00
Lovegood
847cacc71e Update README.md 2025-01-24 10:58:10 +08:00
Lovegood
23149b8a28 Update README.md 2025-01-24 10:57:29 +08:00
Lovegood
698f496a3c Update README.md 2025-01-24 10:57:09 +08:00
Lovegood
8ac97f43ff Update README.md 2025-01-24 10:55:33 +08:00
Lovegood
0abf7c9e5a Update README.md 2025-01-24 10:54:00 +08:00
spark
a55920f445 feat(backend): 添加电报新闻功能
- 新增 GetTelegraphList 函数,用于获取电报新闻列表
- 在处理用户消息时,添加了获取电报新闻的协程
- 优化了消息处理流程,增加了电报新闻的回复
2025-01-23 17:29:18 +08:00
spark
775635a48c feat(backend): 添加通用聊天流功能并优化系统托盘事件处理
- 在 openai_api.go 中添加 NewCommonChatStream 函数,实现通用聊天流功能
- 修改 systray.Run 调用,使用 goroutine 异步执行 onReady 和 onExit 函数- 更新 stock.vue 中的 search函数,增加对多个股票信息页面的支持
2025-01-23 17:07:33 +08:00
spark
5bc7cfab0a feat(app): 更新股票信息显示和隐藏功能
- 在股票信息更新时,如果总价格不为0,设置系统托盘提示信息- 修复了显示和隐藏应用程序的功能
- 优化了股票数据 API 的请求 URL
- 替换 ioutil 包为 io 包,以适应 Go 1.16 及以上版本
2025-01-23 11:18:11 +08:00
spark
e3e06d342b feat(backend): 添加股票价格信息查询功能
- 新增 SearchStockPriceInfo 函数,用于查询股票价格信息
- 更新 NewChatStream 函数,增加股票代码参数- 在前端添加股票代码参数传递
- 优化后端接口测试用例
2025-01-22 17:00:14 +08:00
spark
16e187b96c docs: 更新 README 中的设置截图
- 将 README.md 中的 img.png 文件引用从根目录改为 build/screenshot 目录
- 更新设置截图,使用最新的 img_11.png 替换旧的 img.png
2025-01-22 15:41:47 +08:00
spark
399513cf14 feat(backend): 添加股票信息搜索功能并优化 OpenAI API调用
- 新增 SearchStockInfo 函数,用于搜索指定股票的相关信息
- 优化 OpenAI API 调用,使用搜索到的股票信息作为上下文- 更新 go.mod 和 go.sum 文件,添加 chromedp 等依赖
2025-01-22 15:15:41 +08:00
spark
3f024faf82 feat(frontend): 添加 AI 分析开关功能
- 在全局配置中获取 openAiEnable 状态
- 根据 openAiEnable 状态控制 AI 分析按钮的显示- 优化了组件的初始化逻辑,确保配置信息及时加载
2025-01-22 12:21:43 +08:00
spark
dadfe1cf54 feat(frontend): 实现 AI聊天流功能
- 新增 NewChatStream 函数,用于接收实时聊天流数据
- 在 App 组件中添加 NewChatStream 方法处理聊天流
- 修改前端 Stock 组件,支持实时显示 AI 聊天流结果
- 优化后端 OpenAi 结构,增加 NewChatStream 方法获取流式响应
2025-01-22 12:02:33 +08:00
spark
9cd6761778 feat(backend): 移除 OpenAI API 中的 Markdown 输出
- 删除了 OpenAI API 请求中的 Markdown 输出要求
- 注释掉了日志记录响应内容的代码行- 在 README 中添加了关于 AI 股票分析功能的重大更新说明
2025-01-17 15:33:04 +08:00
spark
1d58d6b224 feat(backend): 移除 OpenAI API 中的 Markdown 输出
- 删除了 OpenAI API 请求中的 Markdown 输出要求
- 注释掉了日志记录响应内容的代码行- 在 README 中添加了关于 AI 股票分析功能的重大更新说明
2025-01-17 15:29:57 +08:00
spark
db4a2b5fa9 feat(backend): 更新 OpenAI API 调用以支持 Markdown 输出
- 在请求内容中添加了对 Markdown 输出的要求
- 此更改将提高回复的可读性和格式化效果
2025-01-17 15:21:17 +08:00
spark
af3f2b03dc style(frontend): 优化 AI 分析结果弹窗样式
-调整弹窗高度为480px,增加垂直空间
- 设置 Markdown 预览区域高度为 380px,确保内容显示完整
-移除不必要的 previewTheme 属性,直接使用 theme 属性
2025-01-17 15:19:46 +08:00
spark
ccbb835c83 feat(frontend): 集成 OpenAI 聊天功能- 新增 NewChat 函数,用于与 OpenAI 进行聊天
- 在 App.d.ts 和 App.js 中添加 NewChat 方法的声明和实现
- 在 models.ts 中添加 OpenAI 相关的配置项
- 在 package.json 中添加 md-editor-v3 依赖,可能用于富文本编辑
2025-01-17 14:36:13 +08:00
spark
97b5faee4a refactor(app): 优化系统托盘相关代码
- 移除了 systray.Run 函数的冗余 goroutine
- 删除了多余的空行,提高了代码可读性
2025-01-15 13:01:12 +08:00
spark
c9a8192d60 style: 修改 .gitignore 文件中的路径格式
- 移除了 .gitignore 文件中路径开头的 ./
- 使路径格式更加规范和一致
2025-01-15 10:34:42 +08:00
spark
1af138312a chore: 更新 .gitignore 文件
- 移除对特定可执行文件的忽略规则
- 使用通配符忽略 build/bin 目录下的所有文件
- 删除 frontend/package.json.md5 忽略规则
2025-01-15 10:32:01 +08:00
spark
272c990248 build: 更新 .gitignore 文件- 添加 frontend/package.json.md5到忽略列表,避免无关文件影响版本控制 2025-01-15 10:29:31 +08:00
spark
2907285915 build: 更新 .gitignore 文件- 添加 frontend/package.json.md5到忽略列表,避免无关文件影响版本控制 2025-01-15 10:28:40 +08:00
sparkmemory
9f7b7b8a64 feat(app): 启动时添加股票价格监控
- 在 app.go 中添加了 MonitorStockPrices 函数的异步调用
- 修改了前端 App.vue 中的跑马灯效果,包括速度、样式和布局调整
- 更新了 package.json 的 MD5 哈希值
2025-01-14 23:42:43 +08:00
spark
02bfe4758e refactor(app): 调整系统托盘创建逻辑并更新应用配置
- 将系统托盘创建逻辑从 main.go 移动到 app.go 中的 startup 方法- 更新应用配置,添加生产环境日志级别配置
- 移除 main.go 中的冗余注释
2025-01-14 21:03:35 +08:00
spark
6483243d2a feat(stock): 添加电报资讯功能
- 在后端增加电报资讯抓取功能,定时刷新并发送到前端
- 在前端添加电报资讯显示组件,滚动显示最新资讯
- 更新 go.mod 和 go.sum 文件,添加相关依赖
2025-01-14 13:13:50 +08:00
spark
1ea534b3c0 refactor(app): 重构应用启动和托盘功能
- 移除 App.startup 中的系统托盘创建逻辑
- 在 main.go 中添加系统托盘创建逻辑- 更新前端 App.vue,添加实时盈亏显示和相关事件监听- 调整 stock.vue,引入通知功能
2025-01-14 11:31:15 +08:00
spark
2fcd89ab97 style(frontend): 优化页面元素的样式和布局
-调整了 App.vue 中底部菜单栏的 z-index 值- 在 settings.vue 中为数据刷新间隔输入框添加了单位提示- 优化了 stock.vue 中搜索框的布局和样式
2025-01-13 15:23:51 +08:00
spark
b5c44870fe docs: 更新 README 中的屏幕截图链接
- 更新了多个屏幕截图的文件名
- 调整了部分截图的展示顺序- 修正了一个截图链接的路径
2025-01-13 12:30:01 +08:00
spark
54f0a0b585 docs: 更新 README 中的屏幕截图链接
- 更新了多个屏幕截图的文件名
- 调整了部分截图的展示顺序- 修正了一个截图链接的路径
2025-01-13 12:18:27 +08:00
spark
ab6f400930 docs: 更新 README 中的屏幕截图链接
- 更新了多个屏幕截图的文件名
- 调整了部分截图的展示顺序- 修正了一个截图链接的路径
2025-01-13 12:18:08 +08:00
spark
a376d1d92c feat(settings): 添加基础设置功能- 在数据库中增加更新基础信息和刷新间隔的配置项
- 实现根据配置定时更新数据的功能
- 添加启动时更新基础信息的逻辑
- 更新前端设置界面,增加基础设置选项
2025-01-13 12:07:35 +08:00
spark
a653ef9fa8 feat(frontend): 优化 App.vue 中的全屏和隐藏功能- 为全屏和隐藏按钮添加了 title 属性,提升用户体验
- 优化了全屏切换逻辑,现在支持键盘快捷键 Ctrl+F 和 Esc
- 调整了全屏按钮的显示文本,根据当前状态动态变化
- 移除了不必要的 console.log 语句,简化了代码
2025-01-13 10:52:34 +08:00
sparkmemory
ce29514b54 feat(settings): 优化配置更新逻辑
- 增加对配置存在的检查,如果存在则更新,不存在则创建默认配置
- 添加日志记录,当配置不存在时创建默认配置的情况
- 通过 Where 子句指定 ID 进行更新,提高更新操作的准确性
2025-01-12 20:47:49 +08:00
sparkmemory
1fd149bbd5 feat(frontend): 添加窗口移动功能并优化错误处理
- 在 App.vue 中添加移动窗口功能
- 优化全屏切换逻辑
- 在 stock_data_api.go 中改进错误处理
- 移除 app.go 中的冗余日志
2025-01-11 22:54:07 +08:00
sparkmemory
9dc8fa97df feat(settings): 添加推送设置功能- 新增本地推送和钉钉推送的配置选项
- 实现配置的保存和读取功能- 添加测试通知按钮
-优化股票信息的显示格式
2025-01-11 14:16:28 +08:00
sparkmemory
7c52cd1d26 style(frontend): 优化页面布局和底部菜单样式
-调整 RouterView 的样式,增加底部填充
- 修改底部菜单的布局方式,从 sticky 改为 fixed,并设置宽度为 100%
- 更新 App.vue 文件中的相关代码
2025-01-10 20:24:12 +08:00
spark
338ce91ffd feat(frontend): 实现基本路由功能并添加设置页面
- 在 App.vue 中集成 vue-router
- 新增 router.js 文件配置路由- 添加设置页面组件和路由- 更新菜单选项,使用 RouterLink 替代普通链接
2025-01-10 18:23:12 +08:00
spark
6e5f57d62e feat(stock): 在股票卡片中添加取消关注按钮
- 在股票卡片的 header-extra 区域添加了一个取消关注按钮
- 按钮点击时调用 removeMonitor 方法,传入股票代码、名称和 key
- 移除了原有的可关闭图标
2025-01-10 15:46:43 +08:00
spark
b1a0e9575b feat(frontend): 优化用户界面和功能
- 添加全屏切换功能
- 实现窗口隐藏和退出功能
- 新增设置菜单
- 优化股票信息展示界面
- 调整窗口大小和布局
2025-01-10 15:32:22 +08:00
spark
88fb3ce94c refactor(linux): 移除未使用的 time 包导入
- 删除了 app_linux.go 文件中未使用的 time 包导入
-此修改提高了代码的整洁度和可维护性
2025-01-09 15:51:07 +08:00
spark
60d8efc158 refactor(linux): 移除未使用的 time 包导入
- 删除了 app_linux.go 文件中未使用的 time 包导入
-此修改提高了代码的整洁度和可维护性
2025-01-09 15:47:41 +08:00
spark
a41ab5499a refactor: 为 app_linux.go 添加 time 包导入
- 在 app_linux.go 文件中导入了 time 包
- 此修改可能为后续功能使用时间相关函数做准备
2025-01-09 15:42:47 +08:00
spark
a54f769ea2 feat(app_linux): 增加股票排序和消息发送功能
- 添加 SetStockSort 方法,用于设置股票排序
- 新增 SendDingDingMessageByType 方法,根据消息类型发送钉钉消息- 实现 GenNotificationMsg 方法,生成通知消息内容
- 添加 getMsgTypeTTL 和 getMsgTypeName 方法,用于获取消息类型的 TTL 和名称
- 优化 Greet 方法,处理返回的股票数据
2025-01-09 15:42:02 +08:00
spark
9a46788339 feat(app_linux): 增加股票排序和消息发送功能
- 添加 SetStockSort 方法,用于设置股票排序
- 新增 SendDingDingMessageByType 方法,根据消息类型发送钉钉消息- 实现 GenNotificationMsg 方法,生成通知消息内容
- 添加 getMsgTypeTTL 和 getMsgTypeName 方法,用于获取消息类型的 TTL 和名称
- 优化 Greet 方法,处理返回的股票数据
2025-01-09 15:37:20 +08:00
spark
def92ad722 fix(stock): 修正股票排序键的使用
- 将 result.Sort 修改为 result.sort,以匹配正确的属性名称
- 更新 GetSortKey 函数调用,使用正确的属性名称
2025-01-09 14:54:35 +08:00
spark
7e27996f17 feat(backend): 优化股票数据获取逻辑
- 修改 GetStockCodeRealTimeData 方法,支持批量获取多个股票代码的实时数据
- 新增 GetStockInfos 函数,用于获取关注股票的实时信息- 重构 getStockInfo 函数,提高代码复用性
- 优化数据处理逻辑,提高程序运行效率
2025-01-09 14:45:25 +08:00
spark
ad428f83f8 refactor(stock): 重构股票数据处理逻辑
- 移除定时更新标题的代码
- 优化股票数据获取和处理流程
- 增加更多股票相关信息的计算和展示
- 调整前端组件以适应新的数据结构
- 修复了一些潜在的数值计算问题
2025-01-09 13:31:18 +08:00
spark
d3c6c1d570 feat(app): 优化股票监控和交易时间判断
- 添加了判断是否为交易日和交易时间的函数
- 修改了股票价格更新逻辑,只在交易时间内进行监控
- 优化了股票价格显示,增加了上次当前价格字段
- 更新了前端组件,支持显示股票价格变化动画
2025-01-08 15:28:08 +08:00
spark
1554d3309d feat(backend): 实现股票价格实时监控功能
- 在 App 结构中添加定时更新股票价格的逻辑
- 实现 MonitorStockPrices 函数,用于更新关注股票的价格
- 在前端添加股票价格更新的事件处理
- 优化股票数据的获取和处理逻辑
2025-01-08 14:17:11 +08:00
spark
e7560f3e9b feat(backend): 添加 Windows 系统消息提醒功能
- 新增 AlertWindowsApi 结构体和 SendNotification 方法,用于发送 Windows 系统通知
- 实现 SendDingDingMessageByType 方法,支持根据不同消息类型发送通知
- 添加消息类型 TTL 和名称映射,优化消息发送逻辑
- 更新前端接口,增加 SendDingDingMessageByType 方法调用- 引入 go-toast 库,用于 Windows 系统通知
2025-01-08 10:57:17 +08:00
spark
daa29b37a5 feat(stock): 添加股票排序功能- 新增 SetStockSort 函数用于设置股票排序
- 在前端增加股票排序的输入和显示逻辑
- 修改后端数据库,增加股票排序字段
- 优化股票列表的渲染,支持按排序值进行排序
2025-01-07 13:29:16 +08:00
spark
9a41560bee refactor(frontend): 优化股票组件功能和布局
-调整了固定按钮的位置和样式
- 优化了股票搜索和添加功能的布局
- 移除了不必要的控制台日志输出- 调整了事件处理
2025-01-07 11:11:07 +08:00
spark
975ad611df build(frontend): 升级 naive-ui 至 2.41.0 版本
- 在 package.json 和 package-lock.json 中更新 naive-ui 版本
- 更新 package.json.md5 校验值
2025-01-07 09:54:37 +08:00
spark
b764770729 refactor(app): 重构应用控制逻辑并修正部分功能
-移除了 App.shutdown 方法中的 runtime.Quit 调用
- 将 runtime.Show 替换为 runtime.WindowShow
- 在全屏菜单项中移除了 MenuItem.Hide 调用
- 将 runtime.Hide替换为 runtime.WindowHide
2025-01-07 09:42:20 +08:00
spark
88aa793774 feat(app): 优化应用隐藏功能并添加错误日志
- 注释掉隐藏应用程序的代码,暂时禁用此功能
- 添加对话框错误日志记录,提高错误追踪能力
- 在 shutdown 函数中添加 runtime.Quit 调用,确保应用正确退出
-优化股票组件中的报警逻辑,增加对当前价格的判断
2025-01-07 09:27:24 +08:00
spark
180bec8866 docs: 更新 README.md 文件 2025-01-06 18:07:04 +08:00
spark
420d5b60f1 docs(README): 更新截图并添加钉钉报警通知说明
- 更新成本仓位设置截图
- 添加钉钉报警通知功能截图及说明
2025-01-06 17:59:06 +08:00
spark
af1bc685a7 build(ci): 更新 GitHub Actions 构建配置
- 为 Windows平台构建产物命名为 go-stock-windows-amd64.exe
-为 Linux 平台构建产物命名为 go-stock-linux-amd64- 通过明确指定构建输出名称,提高构建结果的可识别性和一致性
2025-01-06 17:33:54 +08:00
spark
b0922b0878 ci: 更新 GitHub Actions 构建矩阵
- 为 Windows 和 Linux 构建产物添加平台名称后缀
- 保持原有构建配置不变
2025-01-06 17:18:24 +08:00
spark
200a160acf refactor(app): 优化系统托盘和菜单相关代码
- 在 FileMenu 中添加了隐藏到托盘区的功能,仅在 Windows 平台上显示- 优化了代码结构,提高了可读性和可维护性
2025-01-06 16:56:32 +08:00
spark
9fae9fc034 feat: 添加 Linux 平台支持
- 新增 app_linux.go 文件,实现 Linux 平台下的应用逻辑
- 添加缓存功能,用于限制钉钉消息发送频率- 实现股票数据相关功能,包括获取股票列表、关注股票等
- 添加应用启动、关闭等生命周期方法
2025-01-06 16:44:22 +08:00
spark
9a3393bfc3 feat(app): 为 Windows 系统添加系统托盘功能并支持 Linux
- 在 app.go 中添加了对 Windows 操作系统的判断- 仅在 Windows 系统上创建系统托盘
- 更新 GitHub Actions 工作流,添加 Linux 平台的构建
2025-01-06 16:30:03 +08:00
spark
64270d5df2 ci: 更新 Windows 构建输出文件名
- 将 Windows构建输出文件名从 'go-stock' 改为 'go-stock.exe'
- 确保在 Windows 平台上生成可执行文件
2025-01-06 15:19:32 +08:00
spark
e808ca47b6 refactor(app): 注释掉退出相关的代码
- 在 App.d.ts 中注释掉了 systray.Quit() 调用
- 在 App.js 中注释掉了 FileMenu 中的退出选项
- 更新了 models.ts 的 MD5 校验值
2025-01-06 15:18:33 +08:00
sparkmemory
1b3c043ce6 feat(stock): 增加股价提醒功能并优化报警逻辑
- 在 SetAlarmChangePercent 函数中添加 alarmPrice 参数
- 在前端添加股价提醒输入框
- 修改报警逻辑,支持同时根据涨跌幅和股价进行提醒
- 更新数据库模型,添加 AlarmPrice 字段
2025-01-04 20:54:04 +08:00
sparkmemory
04446d7521 refactor(app): 优化菜单项创建顺序和股票收益计算逻辑- 调整 systray 菜单项创建顺序,将"退出"菜单项放在最后- 修正股票收益计算逻辑,确保正确处理负数情况 2025-01-04 19:11:58 +08:00
spark
2306a8e225 ui(systray): 修改系统托盘菜单项名称
-将"隐藏应用程序"菜单项修改为"隐藏"
- 优化菜单项名称,使其更加简洁明了
2025-01-04 14:42:55 +08:00
sparkmemory
46aff404d4 update 2025-01-03 23:19:57 +08:00
sparkmemory
1e5d9bc469 Merge branch 'master' of https://github.com/ArvinLovegood/go-stock
# Conflicts:
#	stock_basic.json
2025-01-03 23:03:40 +08:00
spark
98844ce717 feat(app): 添加系统托盘功能
- 使用 systray 库创建系统托盘图标和菜单- 添加退出、显示和隐藏应用程序的菜单项
- 实现托盘图标初始化和清理逻辑
- 更新 go.mod 和 go.sum 文件,添加相关依赖
2025-01-03 22:54:39 +08:00
spark
2bd91c6555 refactor(frontend): 调整股票数量输入的最小步长为 100
- 在 Stock 组件中的股票数量输入框中添加 step 属性
-将 step 属性设置为 100,以符合业务需求
2025-01-03 17:02:35 +08:00
spark
2166b0a39b feat(stock): 优化股票对话框输入项
- 为成本、数量和涨跌报警值添加单位后缀
- 优化表单项的标签文案
- 调整输入框样式,增加单位后缀
2025-01-03 16:58:47 +08:00
spark
2f2b19f5d7 feat(app): 添加钉钉消息发送功能和股票涨跌报警
- 新增 SendDingDingMessage 和 SetAlarmChangePercent 函数- 实现钉钉消息发送和股票涨跌报警逻辑
- 更新前端界面,增加报警值设置和消息发送功能
- 新增 DingDingAPI 结构体和相关方法
2025-01-03 16:43:32 +08:00
spark
685a7d23b2 feat(stock_data_api): 搜索股票时包含深交所指数
- 将指数市场范围从上交所(SSE)扩展到包括深交所(SZSE)
- 优化了股票和指数的搜索逻辑,提高搜索结果的全面性
2025-01-03 13:17:19 +08:00
spark
5f1eaf02c4 feat(frontend): 扩展股票搜索框占位符文本
- 将搜索框的占位符文本从"请输入股票名称或者代码"修改为"请输入股票/指数名称或者代码"
- 这个修改使得用户更加清晰地知道可以在搜索框中输入股票或指数的名称或代码
2025-01-03 13:05:43 +08:00
spark
116dae19cf feat(stock): 搜索股票时增加指数匹配
- 在搜索股票时增加对上交所指数的匹配
- 优化股票代码输入逻辑,增加空值判断
-调整关注股票功能,避免重复关注
- 修改分时图数据的更新频率为 3.5 秒一次
2025-01-03 13:04:29 +08:00
spark
a35b42f831 refactor(backend): 移除股票数据 API 中的冗余代码
- 删除了 GetIndexBasic 和 GetStockBaseInfo 方法中的冗余代码
- 移除了不必要的文件写入操作和注释掉的代码
- 优化了代码结构,提高了代码的可读性和维护性
2025-01-03 09:57:19 +08:00
spark
513cd69e3e build: 更新 .gitignore 文件以忽略构建输出
- 修改 .gitignore 文件,更新对构建输出文件和目录的忽略规则
-保留了对 .idea 目录和 data/*.db 文件的忽略- 更新了对 build 目录下可执行文件的忽略规则
2025-01-03 09:54:27 +08:00
spark
0ce01bcdf0 build: 更新 .gitignore 文件以忽略构建输出
- 修改 .gitignore 文件,更新对构建输出文件和目录的忽略规则
-保留了对 .idea 目录和 data/*.db 文件的忽略- 更新了对 build 目录下可执行文件的忽略规则
2025-01-03 09:51:45 +08:00
spark
afe5474264 refactor(stock): 优化股票组件和数据更新逻辑
- 修改股票列表显示格式,将代码和名称之间的连接符改为短横线- 调整股票数据更新频率,从 3 秒改为 1 秒
- 修复当前价格为零时的显示问题,使用卖一报价替代
- 优化数据库更新操作,添加 ts_code 条件以确保更新正确性
2025-01-03 09:48:21 +08:00
spark
f35847823b feat(data): 添加指数信息获取功能
- 在 StockDataApi 中新增 GetIndexBasic 方法,用于获取指数信息
- 在数据库中添加 index_basic 表并进行自动迁移- 优化 GetStockBaseInfo 方法,使用 map 结构处理字段- 增加 GetIndexBasic 的单元测试
2025-01-02 17:52:25 +08:00
spark
15120c98da feat(frontend): 优化股票代码输入和搜索功能
- 改进股票列表显示格式,增加连字符分隔
- 添加支持直接输入股票代码进行搜索的功能
- 优化股票选择逻辑,支持不同格式的股票代码
-增加调试日志输出,便于问题排查
2025-01-02 15:39:39 +08:00
sparkmemory
6903abd6f8 update 2024-12-29 07:50:12 +08:00
spark
f9a0c8d94e 样式修改,新增菜单工具栏 2024-12-28 14:41:45 +08:00
spark
0c04272153 样式修改,新增菜单工具栏 2024-12-25 13:13:23 +08:00
sparkmemory
cf7e8415e6 update 2024-12-23 11:26:24 +08:00
sparkmemory
1ab6875790 update 2024-12-23 11:25:33 +08:00
sparkmemory
cc40da8371 update 2024-12-23 11:21:03 +08:00
sparkmemory
29158f51af update 2024-12-23 07:34:58 +08:00
spark
9665462ae5 样式修改 2024-12-20 21:02:37 +08:00
spark
71e3953cd8 添加缩略图 2024-12-20 15:06:09 +08:00
spark
bedeaad1f2 添加按钮位置修改 2024-12-20 14:37:38 +08:00
spark
4f05820e2e 优化首次启动速度 2024-12-20 14:34:24 +08:00
spark
abf05aabd8 退出程序添加提示 2024-12-20 14:33:54 +08:00
spark
9016289e96 样式调整 2024-12-20 11:07:46 +08:00
spark
bab53711b7 添加全屏按钮 2024-12-20 11:01:56 +08:00
spark
de67f07f41 修改为深色主题 2024-12-20 10:01:56 +08:00
spark
869223cfb9 修改为深色主题 2024-12-20 10:00:46 +08:00
spark
d240239fcc 窗口标题显示当前时间 2024-12-19 20:59:01 +08:00
spark
6b62385cad 分时图和K线图展示 2024-12-19 13:48:02 +08:00
74 changed files with 87783 additions and 517 deletions

54
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

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

10
.github/ISSUE_TEMPLATE/custom.md vendored Normal file
View File

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

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

48
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,48 @@
# Pull Request 信息
## 本次 PR 概述
请简要描述这个 Pull Request 做了什么改动。例如:
- 修复了某个特定功能的 bug
- 实现了一个新的功能特性
- 对代码进行了优化,提升了性能
## 相关问题
如果这个 PR 是为了解决某个 Issue请在此处关联对应的 Issue 编号,格式为 `Fixes #<issue-number>`。例如:
Fixes #123
## 改动内容详细说明
### 代码修改
- 列出主要修改的文件和修改点。例如:
- `app_linux.go`
- 修改了函数 `GetStockList` 的逻辑,从使用 `for` 循环改为 `sum` 函数,提升了计算效率。
- `app_test.go`
- 新增了针对 `GetStockList` 函数的单元测试,确保修改后的逻辑正确。
### 新增功能
如果有新增功能,请详细描述该功能的使用方法和特点。例如:
- 新增了一个用户认证模块,支持使用用户名和密码进行登录。使用方法如下:
- 调用 `authenticate_user(username, password)` 函数进行认证。
- 若认证成功,返回 `True`;否则返回 `False`
### 删除内容
如果有删除的代码或文件,请说明删除的原因。例如:
- 删除了 `app_test.go` 文件,因为该模块的功能已经被新的模块替代,不再需要。
## 测试情况
### 单元测试
- 列出运行的单元测试以及测试结果。例如:
- 运行了 `app_test.go` 进行单元测试,所有测试用例均通过。
- 测试覆盖率达到了 90%。
### 集成测试
如果进行了集成测试,请描述测试环境和测试结果。例如:
- 在本地开发环境Wails CLI v2.10.1 node v18.19.1 )中进行了集成测试,功能正常。
- 在 CI/CD 环境中也进行了测试,所有步骤均通过。
## 注意事项
- 提醒其他开发者在审查代码时需要注意的地方。例如:
- 本次修改涉及到数据库表结构的变更,请确保在部署前进行数据库迁移。
- 新增的功能依赖于第三方库 `requests`,请确保在环境中安装该库。
## 其他补充说明
- 可以在这里提供任何其他需要说明的信息,例如设计文档的链接、相关讨论的记录等。

View File

@@ -17,9 +17,12 @@ jobs:
fail-fast: false
matrix:
build:
- name: 'go-stock'
- name: 'go-stock-windows-amd64.exe'
platform: 'windows/amd64'
os: 'windows-latest'
# - name: 'go-stock-linux-amd64'
# platform: 'linux/amd64'
# os: 'ubuntu-latest'
runs-on: ${{ matrix.build.os }}
steps:
@@ -28,11 +31,20 @@ jobs:
with:
submodules: recursive
- name: Build wails
uses: dAppServer/wails-build-action@v2.2
- name: Get commit message
id: get_commit_message
run: |
$commit_message = & git log -1 --pretty=format:"%s"
echo "::set-output name=commit_message::$commit_message"
- name: Build wails x go-stock
uses: ArvinLovegood/wails-build-action@v2.8
id: build
with:
build-name: ${{ matrix.build.name }}
build-platform: ${{ matrix.build.platform }}
package: true
go-version: '1.23'
build-tags: ${{ github.ref_name }}
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
node-version: '18.x'

8
.gitignore vendored
View File

@@ -106,6 +106,8 @@ dist
.DS_Store
.idea/
data/*.db
build/*.exe
/build/bin/go-stock-dev.exe
/data/*.db
/build/*.exe
/build/bin/*
frontend/package.json.md5
/build/us.json

217
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,217 @@
# Contributor Covenant 行为准则
## 我们的承诺
我们作为项目的成员、贡献者和领导者,承诺为每一个人营造一个无骚扰的社区参与环境,无论年龄、体型、可见或不可见的残疾状况、种族、身体特征、性别认同与表达、经验水平、教育背景、社会经济地位、国籍、个人外貌、种族、宗教信仰或性取向如何。
我们承诺以有助于建立一个开放、友好、多元、包容和健康的社区的方式行事和互动。
## 我们的准则
有助于为我们的社区营造积极环境的行为示例包括:
- 对他人展现出同理心和善意
- 尊重不同的意见、观点和经验
- 给予并欣然接受建设性的反馈
- 对自己的错误负责,向受影响的人道歉,并从经验中学习
- 不仅关注个人利益,更着眼于整个社区的利益
不可接受的行为示例包括:
- 使用性暗示的语言或图像,以及任何形式的性关注或挑逗
- 恶意挑衅、侮辱性或贬低性的评论,以及个人或政治攻击
- 公开或私下的骚扰行为
- 在未经明确许可的情况下公布他人的私人信息,如实际地址或电子邮件地址
- 在专业环境中被合理认为不适当的其他行为
## 执行责任
社区领导者有责任阐明和执行我们可接受行为的标准,并将针对任何他们认为不适当、具有威胁性、冒犯性或有害的行为采取适当和公平的纠正措施。
社区领导者有权且有责任移除、编辑或拒绝不符合本行为准则的评论、提交的代码、代码修改、维基编辑、问题报告和其他贡献,并在适当时说明进行管理决策的原因。
## 适用范围
本行为准则适用于所有社区空间,并且当个人在公共场合正式代表社区时也同样适用。代表我们社区的示例包括使用官方电子邮件地址、通过官方社交媒体账户发布内容,或在线上或线下活动中担任指定代表。
## 执行
若发生滥用、骚扰或其他不可接受的行为,可向负责执行的社区领导者报告,邮箱地址为 [sparkmemory@163.com]。所有投诉都将得到及时、公正的审查和调查。
所有社区领导者都有义务尊重任何事件报告者的隐私和安全。
### 执行指南
社区领导者将遵循以下社区影响指南来确定对任何他们认为违反本行为准则的行为的后果:
#### 1. 纠正
**社区影响**:使用不适当的语言或其他被认为在社区中不专业或不受欢迎的行为。
**后果**:社区领导者发出私下的书面警告,阐明违规行为的性质,并解释为什么该行为不适当。可能会要求公开道歉。
#### 2. 警告
**社区影响**:通过单次事件或一系列行为构成的违规。
**后果**:发出警告并说明持续此类行为的后果。在指定的时间段内,禁止与相关人员进行互动,包括主动与执行本行为准则的人员进行互动。这包括避免在社区空间以及社交媒体等外部渠道进行互动。违反这些规定可能会导致临时或永久禁令。
#### 3. 临时禁令
**社区影响**:严重违反社区标准,包括持续的不当行为。
**后果**:在指定的时间段内,禁止与社区进行任何形式的互动或公开交流。在此期间,禁止与相关人员进行任何公开或私下的互动,包括主动与执行本行为准则的人员进行互动。违反这些规定可能会导致永久禁令。
#### 4. 永久禁令
**社区影响**:表现出违反社区标准的行为模式,包括持续的不当行为、骚扰个人,或对某类人群进行攻击或贬低。
**后果**:永久禁止在社区内进行任何形式的公开互动。
## 版权声明
本行为准则改编自 [Contributor Covenant][主页] 2.1 版本,可在 [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1] 查看。
社区影响指南的灵感来自 [Mozilla 的行为准则执行阶梯][Mozilla CoC]。
有关本行为准则常见问题的解答,请参阅常见问题解答页面 [https://www.contributor-covenant.org/faq][FAQ]。该准则有多种语言的翻译版本,可在 [https://www.contributor-covenant.org/translations][翻译] 查看。
[主页]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[翻译]: https://www.contributor-covenant.org/translations
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at [sparkmemory@163.com].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
### Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
#### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
#### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
#### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
#### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

79
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,79 @@
# Contributing to [go-stock]
感谢你对 [go-stock] 项目的兴趣并愿意贡献代码!本指南将帮助你了解如何为这个项目做出贡献。
## 行为准则
在参与这个项目时,请遵守我们的 [行为准则](./CODE_OF_CONDUCT.md)。我们致力于为所有贡献者提供一个友好、包容和尊重的环境。
## 贡献类型
### 报告问题
如果你发现了一个 bug、有功能请求或者对项目有任何建议请在项目的 [GitHub Issues](https://github.com/ArvinLovegood/go-stock/issues) 中创建一个新的 issue。在创建 issue 时,请提供尽可能多的信息,包括:
- **问题描述**:清晰地描述你遇到的问题或建议的功能。
- **重现步骤**:如果是 bug请提供重现该问题的具体步骤。
- **环境信息**:例如操作系统、编程语言版本等。
- **相关日志或错误信息**:如果有的话,请附上相关的日志或错误信息。
### 提交代码
我们欢迎各种类型的代码贡献,包括修复 bug、添加新功能、改进文档等。请按照以下步骤提交你的代码
#### 1. Fork 项目
在 GitHub 上点击项目页面的 “Fork” 按钮,将项目复制到你自己的 GitHub 账户下。
#### 2. 克隆项目到本地
使用以下命令将你 fork 的项目克隆到本地:
```bash
git clone https://github.com/ArvinLovegood/go-stock.git
cd go-stock
```
#### 3. 创建新分支
在开始编写代码之前,创建一个新的分支来包含你的更改。建议使用一个描述性的分支名称,例如 `fix-bug-123``add-new-feature`
```bash
git checkout -b 新分支名称
```
#### 4. 编写代码
在新分支上进行你的代码更改。请确保你的代码遵循项目的编码风格和规范。
#### 5. 测试代码
在提交代码之前,请确保你的更改通过了项目的测试。如果项目没有测试,请考虑添加适当的测试。
#### 6. 提交更改
将你的更改提交到本地仓库,并提供一个清晰、简洁的提交信息。
```bash
git add.
git commit -m "描述你的更改,例如:修复了 #123 号 bug"
```
#### 7. 同步上游仓库
在推送代码之前,确保你的分支与上游仓库(原始项目)保持同步。
```bash
git remote add upstream https://github.com/ArvinLovegood/go-stock.git
git fetch upstream
git rebase upstream/main
```
#### 8. 推送更改
将你的更改推送到你 fork 的 GitHub 仓库。
```bash
git push origin 新分支名称
```
#### 9. 创建 Pull Request
在 GitHub 上,导航到你 fork 的项目页面,点击 “New pull request” 按钮。选择你刚刚推送的分支,并提供一个清晰的描述,说明你的更改内容和目的。然后提交 pull request。
### 改进文档
良好的文档对于项目的成功至关重要。如果你发现文档中有错误、不清楚的地方或者有可以改进的地方,请提交一个 issue 或者直接修改文档并提交 pull request。
## 代码风格和规范
请遵循项目的代码风格和规范。如果项目中没有明确的规范,请参考以下通用准则:
- **代码格式**:使用一致的缩进、空格和换行符。
- **注释**:添加适当的注释来解释代码的功能和逻辑。
- **命名规范**:使用有意义的变量名、函数名和类名。
## 许可证
通过贡献代码,你同意你的贡献将根据项目的 [许可证](./LICENSE) 进行分发。
再次感谢你对项目的贡献!如果你有任何问题或需要帮助,请随时在 issue 中提问。

View File

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

119
README.md
View File

@@ -1,31 +1,114 @@
# README
# ![go-stock](./build/appicon.png)
## go-stock : 基于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](https://gitcode.com/ArvinLovegood/go-stock/star/badge.svg)](https://gitcode.com/ArvinLovegood/go-stock)
### ✨ 简介
- 本项目基于Wails和NaiveUI开发结合AI大模型构建的股票分析工具。
- 支持市场整体/个股情绪分析K线技术指标分析等功能。
- 本项目仅供娱乐不喜勿喷AI分析股票结果仅供学习研究投资有风险请谨慎使用。
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。
![Wails and NaiveUI](./build/appicon.png)
### 📦 立即体验
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
## About
### 💬 支持大模型/平台
| 模型 | 状态 | 备注 |
| --- | --- |-----------------------------------------------------------------------------------------------------------------------------------------------------|
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner模型测试有问题可通过本地模型或聚合模型平台使用 |
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk)[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) |
A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
A股数据可视化工具基于Wails和NaiveUI。
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- 火山方舟每个模型注册即送50万tokens[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究,[注册链接](https://tushare.pro/register?reg=701944)
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
- 欢迎大家提出宝贵的建议欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
## Prerequisites
INSTALL [GO](https://golang.org) AND [Wails](https://wails.io/)
## Running the Application in Developer Mode
The easiest way is to use the Wails CLI: `wails dev`
## 🧩 重大功能开发计划
| 功能说明 | 状态 | 备注 |
|-----------------|----|--------------------------------------------------------------------------------------------------------|
| ETF支持 | 🚧 | ETF数据支持 |
| 美股支持 | ✅ | 美股数据支持 |
| 港股支持 | ✅ | 港股数据支持 (目前有延迟) |
| 多轮对话 | ✅ | AI分析后可继续对话提问 |
| 自定义AI分析提问模板 | ✅ | 可配置的提问模板 [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha) |
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
This should hot refresh when making changes the Frontend and rebuild when making changes in the Go.
## 👀 更新日志
### 2025.02.28 美股数据支持
### 2025.02.23 弹幕功能,盯盘不再孤单,无聊划个水!😎
### 2025.02.22 港股数据支持(目前有延迟)
### 2025.02.16 AI分析后可继续对话提问
- [v2025.2.16.1-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.16.1-alpha)
### 2025.02.12 可配置的提问模板
- [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha)
## 🦄 重大更新
### BIG NEWS !!! 重大更新!!!
- 2025.01.17 新增AI大模型分析股票功能
![img_5.png](build/screenshot/img.png)
## 📸 功能截图
![img_1.png](build/screenshot/img_6.png)
### 设置
![img_12.png](build/screenshot/img_4.png)
### 成本设置
![img.png](build/screenshot/img_7.png)
### 日K
![img_2.png](build/screenshot/img_8.png)
### 分时
![img_3.png](build/screenshot/img_9.png)
### 钉钉报警通知
![img_4.png](build/screenshot/img_5.png)
### AI分析股票
![img_5.png](build/screenshot/img.png)
### 版本信息提示
![img_11.png](build/screenshot/img_11.png)
## 💕 感谢以下项目
- [NaiveUI](https://www.naiveui.com/)
- [Wails](https://wails.io/)
- [Vue](https://vuejs.org/)
- [Vite](https://vitejs.dev/)
- [Tushare](https://tushare.pro/register?reg=701944)
## 😘 赞助我
### 都划到这了,如果我的项目对您有帮助,请赞助我吧!😊😊😊
| 支付宝 | 微信 |
|-----|-----|
| ![alipay.jpg](build/screenshot/alipay.jpg) | ![wxpay.jpg](build/screenshot/wxpay.jpg) |
## ⭐ Star History
[![Star History Chart](https://api.star-history.com/svg?repos=ArvinLovegood/go-stock&type=Date)](https://star-history.com/#ArvinLovegood/go-stock&Date)
## 🤖 状态
![Alt](https://repobeats.axiom.co/api/embed/40b07d415a42c2264a18c4fe1b6f182ff1470687.svg "Repobeats analytics image")
## 🐳 关于技术支持申明
- 本软件基于开源技术构建使用Wails、NaiveUI、Vue、AI大模型等开源项目。 技术上如有问题,可以先向对应的开源社区请求帮助。
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系微信(备注 技术支持)ArvinLovegood
<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood">
| 技术支持方式 | 赞助(元) |
|:--------------------------------|:-----:|
| 加 QQ506808970微信ArvinLovegood | 100/次 |
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |
## Building the Application for Production
You can build you Application with: `wails build`
## License
[Apache License 2.0](LICENSE)
## Credits
[NaiveUI](https://www.naiveui.com/)
[Wails](https://wails.io/)
[Vue](https://vuejs.org/)
[Vite](https://vitejs.dev/)

56
SECURITY.md Normal file
View File

@@ -0,0 +1,56 @@
# 安全策略
## 1. 受支持的版本
以下是 [go-stock] 项目当前接受安全更新支持的版本:
| 版本号 | 是否支持 |
| ------- | ------------------ |
| [v年.月.日.版本号] | :white_check_mark: |
| [v较旧的年.月.日.版本号] | :x: |
请注意,通常只有最新的主要或次要版本会积极维护安全更新,较旧版本可能不会收到安全补丁。
## 2. 报告安全漏洞
### 2.1 报告方式
我们非常重视安全漏洞问题。如果您在我们的项目中发现了安全漏洞,请通过以下方式向我们报告:
- **私下披露**:请发送电子邮件至 [sparkmemory@163.com]。在邮件中,请包含以下详细信息:
- 对漏洞的详细描述,包括如何复现该漏洞。
- 受影响的项目版本。
- 该漏洞可能造成的任何影响或风险。
- 如果可能,请提供建议的修复或缓解策略。
### 2.2 响应时间线
- **首次确认**:我们将在收到报告后的 [7] 个工作日内确认收到您的报告。
- **调查与进度更新**:我们将对报告的漏洞进行全面调查,并在 [7] 个工作日内向您提供调查进度更新。
- **补丁发布**:一旦修复方案开发完成,我们将尽快发布补丁。发布补丁的时间可能会因漏洞的复杂程度而有所不同。
### 2.3 保密承诺
我们深知安全漏洞报告保密的重要性。我们将对所有报告内容严格保密,未经您的许可,不会披露您的身份或漏洞的具体细节,除非法律有相关要求。
## 3. 安全更新与沟通
### 3.1 补丁发布
当发现并修复安全漏洞后,我们会为受支持的项目版本发布补丁。补丁将在项目的官方 GitHub 仓库上提供。
### 3.2 安全公告
我们会在项目的 GitHub 安全公告页面发布安全公告。这些公告将详细说明漏洞情况、受影响的版本以及缓解或修复问题的步骤。
### 3.3 沟通渠道
- **GitHub**:所有关于安全更新和公告的官方通知将发布在项目的 GitHub 仓库上。
- **电子邮件**:如果您订阅了项目的安全通知,您将收到有关重要安全更新的电子邮件通知。
## 4. 第三方依赖
我们会定期审查和更新项目中使用的第三方依赖,以确保其安全性。然而,第三方组件的安全性也依赖于其各自的维护者。如果您发现与第三方依赖相关的安全问题,请同时向相应的维护者报告并告知我们。
## 5. 安全最佳实践
我们鼓励项目的所有贡献者和用户遵循以下安全最佳实践:
- 及时更新开发和生产环境,安装最新的安全补丁。
- 使用强大的身份验证和授权机制。
- 避免在代码中硬编码凭证信息。
- 定期审查代码,排查潜在的安全漏洞。
## 6. 联系信息
如果您对 [go-stock] 项目的安全策略有任何疑问或担忧,请通过 [sparkmemory@163.com] 联系我们。

634
app.go
View File

@@ -1,46 +1,447 @@
//go:build windows
package main
import (
"context"
"encoding/base64"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil"
"github.com/getlantern/systray"
"github.com/go-resty/resty/v2"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"golang.org/x/sys/windows/registry"
"os"
"strings"
"syscall"
"time"
)
// App struct
type App struct {
ctx context.Context
ctx context.Context
cache *freecache.Cache
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize)
return &App{
cache: cache,
}
}
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
defer PanicHandler()
runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) {
logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData)
})
logger.SugaredLogger.Infof("Version:%s", Version)
// Perform your setup here
a.ctx = ctx
// 创建系统托盘
systray.Run(func() {
go onReady(a)
}, func() {
go onExit(a)
})
}
func (a *App) CheckUpdate() {
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
if err != nil {
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
tag := &models.Tag{}
_, err = resty.New().R().
SetResult(tag).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/git/ref/tags/" + releaseVersion.TagName)
if err == nil {
releaseVersion.Tag = *tag
}
commit := &models.Commit{}
_, err = resty.New().R().
SetResult(commit).
Get(tag.Object.Url)
if err == nil {
releaseVersion.Commit = *commit
}
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
}
}
// domReady is called after front-end resources have been loaded
func (a App) domReady(ctx context.Context) {
func (a *App) domReady(ctx context.Context) {
defer PanicHandler()
// Add your action here
//定时更新数据
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 {
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() {
a.CheckUpdate()
}()
//检查谷歌浏览器
//go func() {
// f := checkChromeOnWindows()
// if !f {
// go runtime.EventsEmit(a.ctx, "warnMsg", "谷歌浏览器未安装,ai分析功能可能无法使用")
// }
//}()
//检查Edge浏览器
go func() {
path, e := checkEdgeOnWindows()
if !e {
go runtime.EventsEmit(a.ctx, "warnMsg", "Edge浏览器未安装,ai分析功能可能无法使用")
} else {
logger.SugaredLogger.Infof("Edge浏览器已安装路径为: %s", path)
}
}()
}
func refreshTelegraphList() *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, selection.Text())
})
return &telegraph
}
// isTradingDay 判断是否是交易日
func isTradingDay(date time.Time) bool {
weekday := date.Weekday()
// 判断是否是周末
if weekday == time.Saturday || weekday == time.Sunday {
return false
}
// 这里可以添加具体的节假日判断逻辑
// 例如:判断是否是春节、国庆节等
return true
}
// isTradingTime 判断是否是交易时间
func isTradingTime(date time.Time) bool {
if !isTradingDay(date) {
return false
}
hour, minute, _ := date.Clock()
// 判断是否在9:15到11:30之间
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
return true
}
// 判断是否在13:00到15:00之间
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
return true
}
return false
}
// IsHKTradingTime 判断当前时间是否在港股交易时间内
func IsHKTradingTime(date time.Time) bool {
hour, minute, _ := date.Clock()
// 开市前竞价时段09:00 - 09:30
if (hour == 9 && minute >= 0) || (hour == 9 && minute <= 30) {
return true
}
// 上午持续交易时段09:30 - 12:00
if (hour == 9 && minute > 30) || (hour >= 10 && hour < 12) || (hour == 12 && minute == 0) {
return true
}
// 下午持续交易时段13:00 - 16:00
if (hour == 13 && minute >= 0) || (hour >= 14 && hour < 16) || (hour == 16 && minute == 0) {
return true
}
// 收市竞价交易时段16:00 - 16:10
if (hour == 16 && minute >= 0) || (hour == 16 && minute <= 10) {
return true
}
return false
}
// IsUSTradingTime 判断当前时间是否在美股交易时间内
func IsUSTradingTime(date time.Time) bool {
// 获取美国东部时区
est, err := time.LoadLocation("America/New_York")
if err != nil {
logger.SugaredLogger.Errorf("加载时区失败: %s", err.Error())
return false
}
// 将当前时间转换为美国东部时间
estTime := date.In(est)
// 判断是否是周末
weekday := estTime.Weekday()
if weekday == time.Saturday || weekday == time.Sunday {
return false
}
// 获取小时和分钟
hour, minute, _ := estTime.Clock()
// 判断是否在9:30到16:00之间
if (hour == 9 && minute >= 30) || (hour >= 10 && hour < 16) || (hour == 16 && 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 {
if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
continue
}
if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
continue
}
if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
continue
}
total += stockInfo.ProfitAmountToday
price, _ := convertor.ToFloat(stockInfo.Price)
if stockInfo.PrePrice != price {
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
}
}
if total != 0 {
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
systray.SetTooltip(title)
}
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
//runtime.WindowSetTitle(a.ctx, title)
}
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockInfos := make([]data.StockInfo, 0)
stockCodes := make([]string, 0)
for _, follow := range follows {
stockCodes = append(stockCodes, follow.StockCode)
}
stockData, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
for _, info := range *stockData {
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
return follow.StockCode == info.Code
})
if ok {
addStockFollowData(v, &info)
stockInfos = append(stockInfos, info)
}
}
return &stockInfos
}
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
stockCode := follow.StockCode
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
if err != nil || len(*stockDatas) == 0 {
return &data.StockInfo{}
}
stockData := (*stockDatas)[0]
addStockFollowData(follow, &stockData)
return &stockData
}
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
stockData.PrePrice = follow.Price //上次当前价格
stockData.Sort = follow.Sort
stockData.CostPrice = follow.CostPrice //成本价
stockData.CostVolume = follow.Volume //成本量
stockData.AlarmChangePercent = follow.AlarmChangePercent
stockData.AlarmPrice = follow.AlarmPrice
//当前价格
price, _ := convertor.ToFloat(stockData.Price)
//当前价格为0 时 使用卖一价格作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.A1P)
}
//当前价格依然为0 时 使用买一报价作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.B1P)
}
//昨日收盘价
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
//当前价格依然为0 时 使用昨日收盘价为当前价格
if price == 0 {
price = preClosePrice
}
//今日最高价
highPrice, _ := convertor.ToFloat(stockData.High)
if highPrice == 0 {
highPrice, _ = convertor.ToFloat(stockData.Open)
}
//今日最低价
lowPrice, _ := convertor.ToFloat(stockData.Low)
if lowPrice == 0 {
lowPrice, _ = convertor.ToFloat(stockData.Open)
}
//开盘价
//openPrice, _ := convertor.ToFloat(stockData.Open)
if price > 0 {
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
}
if highPrice > 0 {
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
}
if lowPrice > 0 {
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
}
if follow.CostPrice > 0 && follow.Volume > 0 {
if price > 0 {
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
} else {
//未开盘时当前价格为昨日收盘价
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
}
}
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
if follow.Price != price && price > 0 {
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
"price": price,
})
}
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
defer PanicHandler()
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "go-stock",
Message: "确定关闭吗?",
Buttons: []string{"确定"},
Icon: icon,
CancelButton: "取消",
})
if err != nil {
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" {
return true
}
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
defer PanicHandler()
// Perform your teardown here
systray.Quit()
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) *data.StockInfo {
stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(name)
func (a *App) Greet(stockCode string) *data.StockInfo {
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
follow := &data.FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
stockInfo := getStockInfo(*follow)
return stockInfo
}
@@ -63,3 +464,226 @@ func (a *App) GetStockList(key string) []data.StockBasic {
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
}
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
}
func (a *App) SetStockSort(sort int64, stockCode string) {
data.NewStockDataApi().SetStockSort(sort, stockCode)
}
func (a *App) SendDingDingMessage(message string, stockCode string) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
stockInfo := &data.StockInfo{}
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChatStream(stock, stockCode, question string) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
}
func (a *App) SaveAIResponseResult(stockCode, stockName, result, chatId, question string) {
data.NewDeepSeekOpenAi(a.ctx).SaveAIResponseResult(stockCode, stockName, result, chatId, question)
}
func (a *App) GetAIResponseResult(stock string) *models.AIResponseResult {
return data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stock)
}
func (a *App) GetVersionInfo() *models.VersionInfo {
return &models.VersionInfo{
Version: Version,
Icon: GetImageBase(icon),
Alipay: GetImageBase(alipay),
Wxpay: GetImageBase(wxpay),
Content: VersionCommit,
}
}
// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装
func checkChromeOnWindows() bool {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
if err != nil {
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer key.Close()
}
defer key.Close()
_, _, err = key.GetValue("Path", nil)
return err == nil
}
// checkEdgeOnWindows 在 Windows 系统上检查Edge浏览器是否安装并返回安装路径
func checkEdgeOnWindows() (string, bool) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
if err != nil {
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
if err != nil {
return "", false
}
defer key.Close()
}
defer key.Close()
path, _, err := key.GetStringValue("Path")
if err != nil {
return "", false
}
return path, true
}
func GetImageBase(bytes []byte) string {
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(bytes)
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {
Price = 0
}
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
if err != nil {
PreClose = 0
}
var RF float64
if PreClose > 0 {
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
}
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
}
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
func getMsgTypeTTL(msgType int) int {
switch msgType {
case 1:
return 60 * 5
case 2:
return 60 * 30
case 3:
return 60 * 30
default:
return 60 * 5
}
}
func getMsgTypeName(msgType int) string {
switch msgType {
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return "未知类型"
}
}
func onExit(a *App) {
// 清理操作
logger.SugaredLogger.Infof("onExit")
runtime.Quit(a.ctx)
}
func onReady(a *App) {
// 初始化操作
logger.SugaredLogger.Infof("onReady")
systray.SetIcon(icon2)
systray.SetTitle("go-stock")
systray.SetTooltip("go-stock 股票行情实时获取")
// 创建菜单项
show := systray.AddMenuItem("显示", "显示应用程序")
hide := systray.AddMenuItem("隐藏", "隐藏应用程序")
systray.AddSeparator()
mQuitOrig := systray.AddMenuItem("退出", "退出应用程序")
// 监听菜单项点击事件
go func() {
for {
select {
case <-mQuitOrig.ClickedCh:
logger.SugaredLogger.Infof("退出应用程序")
runtime.Quit(a.ctx)
//systray.Quit()
case <-show.ClickedCh:
logger.SugaredLogger.Infof("显示应用程序")
runtime.WindowShow(a.ctx)
//runtime.WindowShow(a.ctx)
case <-hide.ClickedCh:
logger.SugaredLogger.Infof("隐藏应用程序")
runtime.WindowHide(a.ctx)
}
}
}()
}
func (a *App) UpdateConfig(settings *data.Settings) string {
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}
func (a *App) ExportConfig() string {
config := data.NewSettingsApi(&data.Settings{}).Export()
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "导出配置文件",
CanCreateDirectories: true,
DefaultFilename: "config.json",
})
if err != nil {
logger.SugaredLogger.Errorf("导出配置文件失败:%s", err.Error())
return err.Error()
}
err = os.WriteFile(file, []byte(config), 0644)
if err != nil {
logger.SugaredLogger.Errorf("导出配置文件失败:%s", err.Error())
return err.Error()
}
return "导出成功:" + file
}
func getScreenResolution() (int, int, error) {
user32 := syscall.NewLazyDLL("user32.dll")
getSystemMetrics := user32.NewProc("GetSystemMetrics")
width, _, _ := getSystemMetrics.Call(0)
height, _, _ := getSystemMetrics.Call(1)
return int(width), int(height), nil
}

459
app_darwin.go Normal file
View File

@@ -0,0 +1,459 @@
//go:build darwin
// +build darwin
package main
import (
"context"
"fmt"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/slice"
"github.com/go-resty/resty/v2"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
}
// NewApp creates a new App application struct
func NewApp() *App {
cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize)
return &App{
cache: cache,
}
}
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
logger.SugaredLogger.Infof("Version:%s", Version)
// Perform your setup here
a.ctx = ctx
// TODO 创建系统托盘
}
func checkUpdate(a *App) {
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
if err != nil {
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
}
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
//定时更新数据
go func() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
interval := config.RefreshInterval
if interval <= 0 {
interval = 1
}
ticker := time.NewTicker(time.Second * time.Duration(interval))
defer ticker.Stop()
for range ticker.C {
if isTradingTime(time.Now()) {
MonitorStockPrices(a)
}
}
}()
go func() {
ticker := time.NewTicker(time.Second * time.Duration(60))
defer ticker.Stop()
for range ticker.C {
telegraph := refreshTelegraphList()
if telegraph != nil {
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
}
}
}()
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
go MonitorStockPrices(a)
//检查新版本
go func() {
checkUpdate(a)
}()
}
func refreshTelegraphList() *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, selection.Text())
})
return &telegraph
}
// isTradingDay 判断是否是交易日
func isTradingDay(date time.Time) bool {
weekday := date.Weekday()
// 判断是否是周末
if weekday == time.Saturday || weekday == time.Sunday {
return false
}
// 这里可以添加具体的节假日判断逻辑
// 例如:判断是否是春节、国庆节等
return true
}
// isTradingTime 判断是否是交易时间
func isTradingTime(date time.Time) bool {
if !isTradingDay(date) {
return false
}
hour, minute, _ := date.Clock()
// 判断是否在9:15到11:30之间
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
return true
}
// 判断是否在13:00到15:00之间
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
return true
}
return false
}
func MonitorStockPrices(a *App) {
dest := &[]data.FollowedStock{}
db.Dao.Model(&data.FollowedStock{}).Find(dest)
total := float64(0)
//for _, follow := range *dest {
// stockData := getStockInfo(follow)
// total += stockData.ProfitAmountToday
// price, _ := convertor.ToFloat(stockData.Price)
// if stockData.PrePrice != price {
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
// }
//}
stockInfos := GetStockInfos(*dest...)
for _, stockInfo := range *stockInfos {
total += stockInfo.ProfitAmountToday
price, _ := convertor.ToFloat(stockInfo.Price)
if stockInfo.PrePrice != price {
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
}
}
if total != 0 {
// title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
// systray.SetTooltip(title)
}
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
//runtime.WindowSetTitle(a.ctx, title)
}
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockCodes := make([]string, 0)
for _, follow := range follows {
stockCodes = append(stockCodes, follow.StockCode)
}
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
if err != nil {
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
return nil
}
stockInfos := make([]data.StockInfo, 0)
for _, info := range *stockData {
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
return follow.StockCode == info.Code
})
if ok {
addStockFollowData(v, &info)
stockInfos = append(stockInfos, info)
}
}
return &stockInfos
}
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
stockCode := follow.StockCode
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
if err != nil || len(*stockDatas) == 0 {
return &data.StockInfo{}
}
stockData := (*stockDatas)[0]
addStockFollowData(follow, &stockData)
return &stockData
}
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
stockData.PrePrice = follow.Price //上次当前价格
stockData.Sort = follow.Sort
stockData.CostPrice = follow.CostPrice //成本价
stockData.CostVolume = follow.Volume //成本量
stockData.AlarmChangePercent = follow.AlarmChangePercent
stockData.AlarmPrice = follow.AlarmPrice
//当前价格
price, _ := convertor.ToFloat(stockData.Price)
//当前价格为0 时 使用卖一价格作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.A1P)
}
//当前价格依然为0 时 使用买一报价作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.B1P)
}
//昨日收盘价
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
//当前价格依然为0 时 使用昨日收盘价为当前价格
if price == 0 {
price = preClosePrice
}
//今日最高价
highPrice, _ := convertor.ToFloat(stockData.High)
if highPrice == 0 {
highPrice, _ = convertor.ToFloat(stockData.Open)
}
//今日最低价
lowPrice, _ := convertor.ToFloat(stockData.Low)
if lowPrice == 0 {
lowPrice, _ = convertor.ToFloat(stockData.Open)
}
//开盘价
//openPrice, _ := convertor.ToFloat(stockData.Open)
if price > 0 {
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
}
if highPrice > 0 {
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
}
if lowPrice > 0 {
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
}
if follow.CostPrice > 0 && follow.Volume > 0 {
if price > 0 {
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
} else {
//未开盘时当前价格为昨日收盘价
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
}
}
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
if follow.Price != price && price > 0 {
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
"price": price,
})
}
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "go-stock",
Message: "确定关闭吗?",
Buttons: []string{"确定"},
Icon: icon,
CancelButton: "取消",
})
if err != nil {
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" {
return true
}
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
// systray.Quit()
}
// Greet returns a greeting for the given name
func (a *App) Greet(stockCode string) *data.StockInfo {
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
follow := &data.FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
stockInfo := getStockInfo(*follow)
return stockInfo
}
func (a *App) Follow(stockCode string) string {
return data.NewStockDataApi().Follow(stockCode)
}
func (a *App) UnFollow(stockCode string) string {
return data.NewStockDataApi().UnFollow(stockCode)
}
func (a *App) GetFollowList() []data.FollowedStock {
return data.NewStockDataApi().GetFollowList()
}
func (a *App) GetStockList(key string) []data.StockBasic {
return data.NewStockDataApi().GetStockList(key)
}
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
}
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
}
func (a *App) SetStockSort(sort int64, stockCode string) {
data.NewStockDataApi().SetStockSort(sort, stockCode)
}
func (a *App) SendDingDingMessage(message string, stockCode string) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
stockInfo := &data.StockInfo{}
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChat(stock string) string {
return data.NewDeepSeekOpenAi().NewChat(stock)
}
func (a *App) NewChatStream(stock, stockCode string) {
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {
Price = 0
}
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
if err != nil {
PreClose = 0
}
var RF float64
if PreClose > 0 {
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
}
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
}
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
func getMsgTypeTTL(msgType int) int {
switch msgType {
case 1:
return 60 * 5
case 2:
return 60 * 30
case 3:
return 60 * 30
default:
return 60 * 5
}
}
func getMsgTypeName(msgType int) string {
switch msgType {
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return "未知类型"
}
}
func (a *App) UpdateConfig(settings *data.Settings) string {
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}

193
app_linux.go Normal file
View File

@@ -0,0 +1,193 @@
//go:build linux
// +build linux
package main
import (
"context"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/logger"
)
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
}
// NewApp creates a new App application struct
func NewApp() *App {
cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize)
return &App{
cache: cache,
}
}
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
// Perform your setup here
a.ctx = ctx
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
//ticker := time.NewTicker(time.Second)
//defer ticker.Stop()
////定时更新数据
//go func() {
// for range ticker.C {
// runtime.WindowSetTitle(ctx, "go-stock "+time.Now().Format("2006-01-02 15:04:05"))
// }
//}()
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "go-stock",
Message: "确定关闭吗?",
Buttons: []string{"确定"},
Icon: icon,
CancelButton: "取消",
})
if err != nil {
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" {
return true
}
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) *data.StockInfo {
stockDatas, _ := data.NewStockDataApi().GetStockCodeRealTimeData(name)
stockData := (*stockDatas)[0]
return &stockData
}
func (a *App) Follow(stockCode string) string {
return data.NewStockDataApi().Follow(stockCode)
}
func (a *App) UnFollow(stockCode string) string {
return data.NewStockDataApi().UnFollow(stockCode)
}
func (a *App) GetFollowList() []data.FollowedStock {
return data.NewStockDataApi().GetFollowList()
}
func (a *App) GetStockList(key string) []data.StockBasic {
return data.NewStockDataApi().GetStockList(key)
}
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
}
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
}
func (a *App) SendDingDingMessage(message string, stockCode string) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) SetStockSort(sort int64, stockCode string) {
data.NewStockDataApi().SetStockSort(sort, stockCode)
}
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {
Price = 0
}
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
if err != nil {
PreClose = 0
}
var RF float64
if PreClose > 0 {
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
}
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
}
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
func getMsgTypeTTL(msgType int) int {
switch msgType {
case 1:
return 60 * 5
case 2:
return 60 * 30
case 3:
return 60 * 30
default:
return 60 * 5
}
}
func getMsgTypeName(msgType int) string {
switch msgType {
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return "未知类型"
}
}
func (a *App) UpdateConfig(settings *data.Settings) string {
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}

19
app_test.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"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) {
t.Log(IsUSTradingTime(time.Now()))
}

View File

@@ -0,0 +1,52 @@
//go:build darwin
// +build darwin
package data
import (
"fmt"
"go-stock/backend/logger"
"os/exec"
)
// AlertWindowsApi @Author 2lovecode
// @Date 2025/02/06 17:50
// @Desc
// -----------------------------------------------------------------------------------
type AlertWindowsApi struct {
AppID string
// 窗口标题
Title string
// 窗口内容
Content string
// 窗口图标
Icon string
}
func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string) *AlertWindowsApi {
return &AlertWindowsApi{
AppID: AppID,
Title: Title,
Content: Content,
Icon: Icon,
}
}
func (a AlertWindowsApi) SendNotification() bool {
if getConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}
script := fmt.Sprintf(`display notification "%s" with title "%s"`, a.Content, a.Title)
cmd := exec.Command("osascript", "-e", script)
err := cmd.Run()
if err != nil {
logger.SugaredLogger.Error(err)
return false
}
return true
}

View File

@@ -0,0 +1,32 @@
//go:build darwin
// +build darwin
package data
import (
"go-stock/backend/logger"
"testing"
"github.com/go-toast/toast"
)
// @Author 2lovecode
// @Date 2025/02/06 17:50
// @Desc
// -----------------------------------------------------------------------------------
func TestAlert(t *testing.T) {
notification := toast.Notification{
AppID: "go-stock",
Title: "Hello, World!",
Message: "This is a toast notification.",
Icon: "../../build/appicon.png",
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
return
}
}

View File

@@ -0,0 +1,53 @@
//go:build windows
package data
import (
"github.com/go-toast/toast"
"go-stock/backend/logger"
)
// AlertWindowsApi @Author spark
// @Date 2025/1/8 9:40
// @Desc
// -----------------------------------------------------------------------------------
type AlertWindowsApi struct {
AppID string
// 窗口标题
Title string
// 窗口内容
Content string
// 窗口图标
Icon string
}
func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string) *AlertWindowsApi {
return &AlertWindowsApi{
AppID: AppID,
Title: Title,
Content: Content,
Icon: Icon,
}
}
func (a AlertWindowsApi) SendNotification() bool {
if getConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}
notification := toast.Notification{
AppID: a.AppID,
Title: a.Title,
Message: a.Content,
Icon: a.Icon,
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
return false
}
return true
}

View File

@@ -0,0 +1,30 @@
//go:build windows
package data
import (
"github.com/go-toast/toast"
"go-stock/backend/logger"
"testing"
)
// @Author spark
// @Date 2025/1/8 9:40
// @Desc
//-----------------------------------------------------------------------------------
func TestAlert(t *testing.T) {
notification := toast.Notification{
AppID: "go-stock",
Title: "Hello, World!",
Message: "This is a toast notification.",
Icon: "../../build/appicon.png",
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
return
}
}

225
backend/data/crawler_api.go Normal file
View File

@@ -0,0 +1,225 @@
package data
import (
"context"
"github.com/chromedp/chromedp"
"go-stock/backend/logger"
"time"
)
// @Author spark
// @Date 2025/2/13 9:25
// @Desc
// -----------------------------------------------------------------------------------
type CrawlerApi struct {
crawlerCtx context.Context
crawlerBaseInfo CrawlerBaseInfo
}
func (c *CrawlerApi) NewTimeOutCrawler(timeout int, crawlerBaseInfo CrawlerBaseInfo) CrawlerApi {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
return c.NewCrawler(ctx, crawlerBaseInfo)
}
func (c *CrawlerApi) NewCrawler(ctx context.Context, crawlerBaseInfo CrawlerBaseInfo) CrawlerApi {
return CrawlerApi{
crawlerCtx: ctx,
crawlerBaseInfo: crawlerBaseInfo,
}
}
func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bool) {
htmlContent := ""
path, e := checkBrowserOnWindows()
logger.SugaredLogger.Infof("GetHtml path:%s", path)
if e {
pctx, pcancel := chromedp.NewExecAllocator(
c.crawlerCtx,
chromedp.ExecPath(path),
chromedp.Flag("headless", headless),
chromedp.Flag("blink-settings", "imagesEnabled=false"),
chromedp.Flag("disable-javascript", false),
chromedp.Flag("disable-gpu", true),
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
chromedp.Flag("disable-background-networking", true),
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
chromedp.Flag("disable-background-timer-throttling", true),
chromedp.Flag("disable-backgrounding-occluded-windows", true),
chromedp.Flag("disable-breakpad", true),
chromedp.Flag("disable-client-side-phishing-detection", true),
chromedp.Flag("disable-default-apps", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
chromedp.Flag("disable-hang-monitor", true),
chromedp.Flag("disable-ipc-flooding-protection", true),
chromedp.Flag("disable-popup-blocking", true),
chromedp.Flag("disable-prompt-on-repost", true),
chromedp.Flag("disable-renderer-backgrounding", true),
chromedp.Flag("disable-sync", true),
chromedp.Flag("force-color-profile", "srgb"),
chromedp.Flag("metrics-recording-only", true),
chromedp.Flag("safebrowsing-disable-auto-update", true),
chromedp.Flag("enable-automation", true),
chromedp.Flag("password-store", "basic"),
chromedp.Flag("use-mock-keychain", true),
)
defer pcancel()
ctx, cancel := chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
defer cancel()
err := chromedp.Run(ctx, chromedp.Navigate(url),
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
chromedp.InnerHTML("body", &htmlContent),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", false
}
} else {
ctx, cancel := chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
defer cancel()
err := chromedp.Run(ctx, chromedp.Navigate(url), chromedp.WaitVisible("body"), chromedp.InnerHTML("body", &htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", false
}
}
return htmlContent, true
}
func (c *CrawlerApi) GetHtmlWithNoCancel(url, waitVisible string, headless bool) (html string, ok bool, parent context.CancelFunc, child context.CancelFunc) {
htmlContent := ""
path, e := checkBrowserOnWindows()
logger.SugaredLogger.Infof("GetHtml path:%s", path)
var parentCancel context.CancelFunc
var childCancel context.CancelFunc
var pctx context.Context
var cctx context.Context
if e {
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, e := checkBrowserOnWindows()
logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
if e {
pctx, pcancel := chromedp.NewExecAllocator(
c.crawlerCtx,
chromedp.ExecPath(path),
chromedp.Flag("headless", headless),
chromedp.Flag("blink-settings", "imagesEnabled=false"),
chromedp.Flag("disable-javascript", false),
chromedp.Flag("disable-gpu", true),
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
chromedp.Flag("disable-background-networking", true),
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
chromedp.Flag("disable-background-timer-throttling", true),
chromedp.Flag("disable-backgrounding-occluded-windows", true),
chromedp.Flag("disable-breakpad", true),
chromedp.Flag("disable-client-side-phishing-detection", true),
chromedp.Flag("disable-default-apps", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
chromedp.Flag("disable-hang-monitor", true),
chromedp.Flag("disable-ipc-flooding-protection", true),
chromedp.Flag("disable-popup-blocking", true),
chromedp.Flag("disable-prompt-on-repost", true),
chromedp.Flag("disable-renderer-backgrounding", true),
chromedp.Flag("disable-sync", true),
chromedp.Flag("force-color-profile", "srgb"),
chromedp.Flag("metrics-recording-only", true),
chromedp.Flag("safebrowsing-disable-auto-update", true),
chromedp.Flag("enable-automation", true),
chromedp.Flag("password-store", "basic"),
chromedp.Flag("use-mock-keychain", true),
)
defer pcancel()
ctx, cancel := chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
defer cancel()
err := chromedp.Run(ctx, *actions...)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", false
}
} else {
ctx, cancel := chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
defer cancel()
err := chromedp.Run(ctx, *actions...)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", false
}
}
return htmlContent, true
}
type CrawlerBaseInfo struct {
Name string `json:"name"`
Description string `json:"description"`
BaseUrl string `json:"base_url"`
Headers map[string]string `json:"headers"`
}

View File

@@ -0,0 +1,310 @@
package data
import (
"context"
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/duke-git/lancet/v2/strutil"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"os"
"strings"
"testing"
"time"
"github.com/chromedp/chromedp"
"github.com/stretchr/testify/assert"
)
func TestNewTimeOutGuShiTongCrawler(t *testing.T) {
crawlerAPI := CrawlerApi{}
timeout := 10
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://gushitong.baidu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
result := crawlerAPI.NewTimeOutCrawler(timeout, crawlerBaseInfo)
assert.NotNil(t, result.crawlerCtx)
assert.Equal(t, crawlerBaseInfo, result.crawlerBaseInfo)
}
func TestNewGuShiTongCrawler(t *testing.T) {
crawlerAPI := CrawlerApi{}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://gushitong.baidu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
result := crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
assert.Equal(t, ctx, result.crawlerCtx)
assert.Equal(t, crawlerBaseInfo, result.crawlerBaseInfo)
}
func TestGetHtml(t *testing.T) {
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://gushitong.baidu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
url := "https://www.cls.cn/searchPage?type=depth&keyword=%E6%96%B0%E5%B8%8C%E6%9C%9B"
waitVisible := ".search-telegraph-list,.subject-interest-list"
//url = "https://gushitong.baidu.com/stock/ab-600745"
//waitVisible = "div.news-item"
htmlContent, success := crawlerAPI.GetHtml(url, waitVisible, true)
if success {
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
var messages []string
document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
messages = append(messages, text)
logger.SugaredLogger.Infof("搜索到消息-%s: %s", "", text)
})
}
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
}
func TestGetHtmlWithActions(t *testing.T) {
crawlerAPI := CrawlerApi{}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, CrawlerBaseInfo{
Name: "百度股市通",
Description: "Test Crawler Description",
BaseUrl: "https://gushitong.baidu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
})
actions := []chromedp.Action{
chromedp.Navigate("https://gushitong.baidu.com/stock/ab-600745"),
chromedp.WaitVisible("div.cos-tab"),
chromedp.Click(".header div.cos-tab:nth-child(6)", chromedp.ByQuery),
chromedp.ScrollIntoView("div.finance-container >div.row:nth-child(3)"),
chromedp.WaitVisible("div.cos-tabs-header-container"),
chromedp.Click(".page-content .cos-tabs-header-container .cos-tabs-header .cos-tab:nth-child(1)", chromedp.ByQuery),
chromedp.WaitVisible(".page-content .finance-container .report-col-content", chromedp.ByQuery),
chromedp.Click(".page-content .cos-tabs-header-container .cos-tabs-header .cos-tab:nth-child(4)", chromedp.ByQuery),
chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight);`, nil),
chromedp.Sleep(1 * time.Second),
}
htmlContent, success := crawlerAPI.GetHtmlWithActions(&actions, false)
if success {
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
var messages []string
document.Find("div.report-table-list-container,div.report-row").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveWhiteSpace(selection.Text(), false)
messages = append(messages, text)
logger.SugaredLogger.Infof("搜索到消息-%s: %s", "", text)
})
logger.SugaredLogger.Infof("messages:%d", len(messages))
}
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
}
func TestHk(t *testing.T) {
//https://stock.finance.sina.com.cn/hkstock/quotes/00001.html
db.Init("../../data/stock.db")
hks := &[]models.StockInfoHK{}
db.Dao.Model(&models.StockInfoHK{}).Limit(1).Find(hks)
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://stock.finance.sina.com.cn",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
for _, hk := range *hks {
logger.SugaredLogger.Infof("hk: %+v", hk)
url := fmt.Sprintf("https://stock.finance.sina.com.cn/hkstock/quotes/%s.html", strings.ReplaceAll(hk.Code, ".HK", ""))
htmlContent, ok := crawlerAPI.GetHtml(url, "#stock_cname", true)
if !ok {
continue
}
//logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
document.Find("#stock_cname").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("股票名称-:%s", text)
})
document.Find("#mts_stock_hk_price").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("股票名称-现价: %s", text)
})
document.Find(".deta_hqContainer >.deta03 li").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("股票名称-%s: %s", "", text)
})
}
}
func TestUpdateUSName(t *testing.T) {
db.Init("../../data/stock.db")
us := &[]models.StockInfoUS{}
db.Dao.Model(&models.StockInfoUS{}).Where("name = ?", "").Order("RANDOM()").Find(us)
for _, us := range *us {
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://stock.finance.sina.com.cn",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", us.Code[:len(us.Code)-3])
logger.SugaredLogger.Infof("url: %s", url)
//waitVisible := "span.quote_title_name"
waitVisible := "div.hq_title > h1"
htmlContent, ok := crawlerAPI.GetHtml(url, waitVisible, true)
if !ok {
continue
}
//logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
name := ""
document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
name = strutil.RemoveNonPrintable(selection.Text())
name = strutil.SplitAndTrim(name, " ", "")[0]
logger.SugaredLogger.Infof("股票名称-:%s", name)
})
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", us.Code).Updates(map[string]interface{}{
"name": name,
"full_name": name,
})
}
}
func TestUS(t *testing.T) {
db.Init("../../data/stock.db")
bytes, err := os.ReadFile("../../build/us.json")
if err != nil {
return
}
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://quote.eastmoney.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
tick := &Tick{}
json.Unmarshal(bytes, &tick)
for i, datum := range tick.Data {
logger.SugaredLogger.Infof("datum: %d, %+v", i, datum)
name := ""
//https://quote.eastmoney.com/us/AAPL.html
//https://stock.finance.sina.com.cn/usstock/quotes/goog.html
//url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", strings.ReplaceAll(datum.C, ".US", ""))
////waitVisible := "span.quote_title_name"
//waitVisible := "div.hq_title > h1"
//
//htmlContent, ok := crawlerAPI.GetHtml(url, waitVisible, true)
//
//if !ok {
// continue
//}
////logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
//document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
//if err != nil {
// logger.SugaredLogger.Error(err.Error())
//}
//document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
// name = strutil.RemoveNonPrintable(selection.Text())
// name = strutil.SplitAndTrim(name, " ", "")[0]
// logger.SugaredLogger.Infof("股票名称-:%s", name)
//})
us := &models.StockInfoUS{
Code: datum.C + ".US",
EName: datum.N,
FullName: datum.N,
Name: name,
Exchange: datum.E,
Type: datum.T,
}
db.Dao.Create(us)
}
}
func TestUSSINA(t *testing.T) {
//https://finance.sina.com.cn/stock/usstock/sector.shtml#cm
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://quote.eastmoney.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
html, ok := crawlerAPI.GetHtml("https://finance.sina.com.cn/stock/usstock/sector.shtml#cm", "div#data", false)
if !ok {
return
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
document.Find("div#data > table >tbody >tr").Each(func(i int, selection *goquery.Selection) {
tr := selection.Text()
logger.SugaredLogger.Infof("tr: %s", tr)
})
}
type Tick struct {
Code int `json:"code"`
Status string `json:"status"`
Data []struct {
C string `json:"c"`
N string `json:"n"`
T string `json:"t"`
E string `json:"e"`
} `json:"data"`
}

View File

@@ -0,0 +1,85 @@
package data
import (
"github.com/go-resty/resty/v2"
"go-stock/backend/logger"
)
// @Author spark
// @Date 2025/1/3 13:53
// @Desc
//-----------------------------------------------------------------------------------
type DingDingAPI struct {
client *resty.Client
}
func NewDingDingAPI() *DingDingAPI {
return &DingDingAPI{
client: resty.New(),
}
}
func (DingDingAPI) SendDingDingMessage(message string) string {
if getConfig().DingPushEnable == false {
logger.SugaredLogger.Info("钉钉推送未开启")
return "钉钉推送未开启"
}
// 发送钉钉消息
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody(message).
Post(getApiURL())
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "发送钉钉消息失败"
}
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
return "发送钉钉消息成功"
}
func getConfig() *Settings {
return NewSettingsApi(&Settings{}).GetConfig()
}
func getApiURL() string {
return getConfig().DingRobot
}
func (DingDingAPI) SendToDingDing(title, message string) string {
// 发送钉钉消息
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody(&Message{
Msgtype: "markdown",
Markdown: Markdown{
Title: "go-stock " + title,
Text: message,
},
At: At{
IsAtAll: true,
},
}).
Post(getApiURL())
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "发送钉钉消息失败"
}
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
return "发送钉钉消息成功"
}
type Message struct {
Msgtype string `json:"msgtype"`
Markdown Markdown `json:"markdown"`
At At `json:"at"`
}
type Markdown struct {
Title string `json:"title"`
Text string `json:"text"`
}
type At struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
}

View File

@@ -0,0 +1,32 @@
package data
import (
"github.com/go-resty/resty/v2"
"testing"
)
// @Author spark
// @Date 2025/1/3 13:53
// @Desc
//-----------------------------------------------------------------------------------
func TestRobot(t *testing.T) {
dingdingRobotUrl := "XXX"
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody(`{
"msgtype": "markdown",
"markdown": {
"title":"go-stock",
"text": "#### 杭州天气 @150XXXXXXXX \n > 9度西北风1级空气良89相对温度73%\n > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n"
},
"at": {
"isAtAll": true
}
}`).
Post(dingdingRobotUrl)
if err != nil {
t.Error(err)
}
t.Log(resp.String())
}

643
backend/data/openai_api.go Normal file
View File

@@ -0,0 +1,643 @@
package data
import (
"bufio"
"context"
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strings"
"sync"
"time"
)
// @Author spark
// @Date 2025/1/16 13:19
// @Desc
// -----------------------------------------------------------------------------------
type OpenAi struct {
ctx context.Context
BaseUrl string `json:"base_url"`
ApiKey string `json:"api_key"`
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
Temperature float64 `json:"temperature"`
Prompt string `json:"prompt"`
TimeOut int `json:"time_out"`
QuestionTemplate string `json:"question_template"`
CrawlTimeOut int64 `json:"crawl_time_out"`
KDays int64 `json:"kDays"`
}
func NewDeepSeekOpenAi(ctx context.Context) *OpenAi {
config := getConfig()
if config.OpenAiEnable {
if config.OpenAiApiTimeOut <= 0 {
config.OpenAiApiTimeOut = 60 * 5
}
if config.CrawlTimeOut <= 0 {
config.CrawlTimeOut = 60
}
if config.KDays < 30 {
config.KDays = 120
}
}
return &OpenAi{
ctx: ctx,
BaseUrl: config.OpenAiBaseUrl,
ApiKey: config.OpenAiApiKey,
Model: config.OpenAiModelName,
MaxTokens: config.OpenAiMaxTokens,
Temperature: config.OpenAiTemperature,
Prompt: config.Prompt,
TimeOut: config.OpenAiApiTimeOut,
QuestionTemplate: config.QuestionTemplate,
CrawlTimeOut: config.CrawlTimeOut,
KDays: config.KDays,
}
}
type THSTokenResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data"`
}
type AiResponse struct {
Id string `json:"id"`
Object string `json:"object"`
Created int `json:"created"`
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
Logprobs interface{} `json:"logprobs"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
PromptCacheHitTokens int `json:"prompt_cache_hit_tokens"`
PromptCacheMissTokens int `json:"prompt_cache_miss_tokens"`
} `json:"usage"`
SystemFingerprint string `json:"system_fingerprint"`
}
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[string]any {
ch := make(chan map[string]any, 512)
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Error("NewChatStream panic", err)
}
}()
go func() {
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Errorf("NewChatStream goroutine panic :%s", err)
logger.SugaredLogger.Errorf("NewChatStream goroutine panic stock:%s stockCode:%s", stock, stockCode)
logger.SugaredLogger.Errorf("NewChatStream goroutine panic config:%v", o)
}
}()
defer close(ch)
msg := []map[string]interface{}{
{
"role": "system",
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合注重风险控制。\n\n【核心功能】\n\n市场分析维度\n\n宏观经济GDP/CPI/货币政策)\n\n行业景气度产业链/政策红利/技术革新)\n\n个股三维诊断\n\n基本面PE/PB/ROE/现金流/护城河\n\n技术面K线形态/均线系统/量价关系/指标背离\n\n资金面主力动向/北向资金/融资余额/大宗交易\n\n智能策略库\n√ 趋势跟踪策略(鳄鱼线+ADX\n√ 波段交易策略(斐波那契回撤+RSI\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系\n▶ 动态止损ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议附压力测试结果\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑3点关键驱动力\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时采用苏格拉底式追问\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式\n📈 标的名称XXXXXX\n⚖ 多空信号:金叉确认/顶背离预警\n🎯 关键价位支撑位XX.XX/压力位XX.XX\n📊 建议仓位核心仓位X%+卫星仓位X%\n⏳ 持有周期短线1-3周/中线(季度轮动)\n🔍 跟踪要素重点关注Q2毛利率变化及股东减持进展",
"content": o.Prompt,
},
}
question := ""
if userQuestion == "" {
replaceTemplates := map[string]string{
"{{stockName}}": RemoveAllBlankChar(stock),
"{{stockCode}}": RemoveAllBlankChar(stockCode),
}
followedStock := &FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(&followedStock).Where("stock_code = ?", stockCode).First(followedStock)
if followedStock.CostPrice > 0 {
replaceTemplates["{{costPrice}}"] = fmt.Sprintf("%.2f", followedStock.CostPrice)
}
question = strutil.ReplaceWithMap(o.QuestionTemplate, replaceTemplates)
} else {
question = userQuestion
}
logger.SugaredLogger.Infof("NewChatStream stock:%s stockCode:%s", stock, stockCode)
logger.SugaredLogger.Infof("Prompt%s", o.Prompt)
logger.SugaredLogger.Infof("User Prompt config:%v", o.QuestionTemplate)
logger.SugaredLogger.Infof("User question:%s", userQuestion)
logger.SugaredLogger.Infof("final question:%s", question)
wg := &sync.WaitGroup{}
wg.Add(6)
go func() {
defer wg.Done()
endDate := time.Now().Format("20060102")
startDate := time.Now().Add(-time.Hour * time.Duration(24*o.KDays)).Format("20060102")
code := stockCode
if strutil.HasPrefixAny(stockCode, []string{"hk", "sz", "sh"}) {
code = ConvertStockCodeToTushareCode(stockCode)
}
K := NewTushareApi(getConfig()).GetDaily(code, startDate, endDate, o.CrawlTimeOut)
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": stock + "日K数据如下\n" + K,
})
}()
go func() {
defer wg.Done()
messages := SearchStockPriceInfo(stockCode, o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票价格失败")
//ch <- "***❗获取股票价格失败,分析结果可能不准确***<hr>"
ch <- map[string]any{
"code": 1,
"question": question,
"extraContent": "***❗获取股票价格失败,分析结果可能不准确***<hr>",
}
go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票价格失败,分析结果可能不准确")
return
}
price := ""
for _, message := range *messages {
price += message + ";"
}
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": stock + time.Now().Format(time.DateOnly) + "价格:" + price,
})
}()
go func() {
defer wg.Done()
if checkIsIndexBasic(stock) {
return
}
messages := GetFinancialReports(stockCode, o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票财报失败")
// "***❗获取股票财报失败,分析结果可能不准确***<hr>"
ch <- map[string]any{
"code": 1,
"question": question,
"extraContent": "***❗获取股票财报失败,分析结果可能不准确***<hr>",
}
go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票财报失败,分析结果可能不准确")
return
}
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": stock + message,
})
}
}()
go func() {
defer wg.Done()
messages := GetTelegraphList(o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取市场资讯失败")
//ch <- "***❗获取市场资讯失败,分析结果可能不准确***<hr>"
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取市场资讯失败,分析结果可能不准确")
return
}
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
}
messages = GetTopNewsList(o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取新闻资讯失败")
//ch <- "***❗获取新闻资讯失败,分析结果可能不准确***<hr>"
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取新闻资讯失败,分析结果可能不准确")
return
}
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
}
}()
//go func() {
// defer wg.Done()
// messages := SearchStockInfo(stock, "depth", o.CrawlTimeOut)
// if messages == nil || len(*messages) == 0 {
// logger.SugaredLogger.Error("获取股票资讯失败")
// //ch <- "***❗获取股票资讯失败,分析结果可能不准确***<hr>"
// //go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票资讯失败,分析结果可能不准确")
// return
// }
// for _, message := range *messages {
// msg = append(msg, map[string]interface{}{
// "role": "assistant",
// "content": message,
// })
// }
//}()
go func() {
defer wg.Done()
messages := SearchStockInfo(stock, "telegram", o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票电报资讯失败")
//ch <- "***❗获取股票电报资讯失败,分析结果可能不准确***<hr>"
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票电报资讯失败,分析结果可能不准确")
return
}
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
}
}()
go func() {
defer wg.Done()
if checkIsIndexBasic(stock) {
return
}
messages := SearchGuShiTongStockInfo(stockCode, o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股势通资讯失败")
//ch <- "***❗获取股势通资讯失败,分析结果可能不准确***<hr>"
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股势通资讯失败,分析结果可能不准确")
return
}
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
}
}()
wg.Wait()
msg = append(msg, map[string]interface{}{
"role": "user",
"content": question,
})
client := resty.New()
client.SetBaseURL(strutil.Trim(o.BaseUrl))
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json")
//client.SetRetryCount(3)
if o.TimeOut <= 0 {
o.TimeOut = 300
}
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
resp, err := client.R().
SetDoNotParseResponse(true).
SetBody(map[string]interface{}{
"model": o.Model,
"max_tokens": o.MaxTokens,
"temperature": o.Temperature,
"stream": true,
"messages": msg,
}).
Post("/chat/completions")
body := resp.RawBody()
defer body.Close()
if err != nil {
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
//ch <- err.Error()
ch <- map[string]any{
"code": 0,
"question": question,
"content": err.Error(),
}
return
}
scanner := bufio.NewScanner(body)
for scanner.Scan() {
line := scanner.Text()
logger.SugaredLogger.Infof("Received data: %s", line)
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
if data == "[DONE]" {
return
}
var streamResponse struct {
Id string `json:"id"`
Model string `json:"model"`
Choices []struct {
Delta struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
} `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
for _, choice := range streamResponse.Choices {
if content := choice.Delta.Content; content != "" {
//ch <- content
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": content,
}
logger.SugaredLogger.Infof("Content data: %s", content)
}
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
//ch <- reasoningContent
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": reasoningContent,
}
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
}
if choice.FinishReason == "stop" {
return
}
}
} else {
if err != nil {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
//ch <- err.Error()
ch <- map[string]any{
"code": 0,
"question": question,
"content": err.Error(),
}
} else {
logger.SugaredLogger.Infof("Stream data error : %s", data)
//ch <- data
ch <- map[string]any{
"code": 0,
"question": question,
"content": data,
}
}
}
} else {
if strutil.RemoveNonPrintable(line) != "" {
logger.SugaredLogger.Infof("Stream data error : %s", line)
res := &models.Resp{}
if err := json.Unmarshal([]byte(line), res); err == nil {
//ch <- line
ch <- map[string]any{
"code": 0,
"question": question,
"content": res.Message,
}
}
}
}
}
}()
return ch
}
func checkIsIndexBasic(stock string) bool {
count := int64(0)
db.Dao.Model(&IndexBasic{}).Where("name = ?", stock).Count(&count)
return count > 0
}
func SearchGuShiTongStockInfo(stock string, crawlTimeOut int64) *[]string {
crawlerAPI := CrawlerApi{}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, CrawlerBaseInfo{
Name: "百度股市通",
BaseUrl: "https://gushitong.baidu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
})
url := "https://gushitong.baidu.com/stock/ab-" + RemoveAllNonDigitChar(stock)
if strutil.HasPrefixAny(stock, []string{"HK", "hk"}) {
url = "https://gushitong.baidu.com/stock/hk-" + RemoveAllNonDigitChar(stock)
}
if strutil.HasPrefixAny(stock, []string{"SZ", "SH", "sh", "sz"}) {
url = "https://gushitong.baidu.com/stock/ab-" + RemoveAllNonDigitChar(stock)
}
if strutil.HasPrefixAny(stock, []string{"us", "US", "gb_", "gb"}) {
url = "https://gushitong.baidu.com/stock/us-" + strings.Replace(stock, "gb_", "", 1)
}
logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索股票-%s: %s", stock, url)
actions := []chromedp.Action{
chromedp.Navigate(url),
chromedp.WaitVisible("div.cos-tab"),
chromedp.Click("div.cos-tab:nth-child(5)", chromedp.ByQuery),
chromedp.ScrollIntoView("div.body-box"),
chromedp.WaitVisible("div.body-col"),
chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight);`, nil),
chromedp.Sleep(1 * time.Second),
}
htmlContent, success := crawlerAPI.GetHtmlWithActions(&actions, true)
var messages []string
if success {
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
document.Find("div.finance-hover,div.list-date").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveWhiteSpace(selection.Text(), false)
messages = append(messages, ReplaceSensitiveWords(text))
logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索到消息-%s: %s", "", text)
})
logger.SugaredLogger.Infof("messages:%d", len(messages))
}
return &messages
}
func GetFinancialReports(stockCode string, crawlTimeOut int64) *[]string {
if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) {
stockCode = strings.ReplaceAll(stockCode, "hk", "")
stockCode = strings.ReplaceAll(stockCode, "HK", "")
}
if strutil.HasPrefixAny(stockCode, []string{"us", "gb_"}) {
stockCode = strings.ReplaceAll(stockCode, "us", "")
stockCode = strings.ReplaceAll(stockCode, "gb_", "")
}
// 创建一个 chromedp 上下文
timeoutCtx, timeoutCtxCancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
defer timeoutCtxCancel()
var ctx context.Context
var cancel context.CancelFunc
path, e := checkBrowserOnWindows()
logger.SugaredLogger.Infof("GetFinancialReports path:%s", path)
if e {
pctx, pcancel := chromedp.NewExecAllocator(
timeoutCtx,
chromedp.ExecPath(path),
chromedp.Flag("headless", true),
chromedp.Flag("disable-javascript", false),
chromedp.Flag("disable-gpu", true),
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"),
chromedp.Flag("disable-background-networking", true),
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
chromedp.Flag("disable-background-timer-throttling", true),
chromedp.Flag("disable-backgrounding-occluded-windows", true),
chromedp.Flag("disable-breakpad", true),
chromedp.Flag("disable-client-side-phishing-detection", true),
chromedp.Flag("disable-default-apps", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
chromedp.Flag("disable-hang-monitor", true),
chromedp.Flag("disable-ipc-flooding-protection", true),
chromedp.Flag("disable-popup-blocking", true),
chromedp.Flag("disable-prompt-on-repost", true),
chromedp.Flag("disable-renderer-backgrounding", true),
chromedp.Flag("disable-sync", true),
chromedp.Flag("force-color-profile", "srgb"),
chromedp.Flag("metrics-recording-only", true),
chromedp.Flag("safebrowsing-disable-auto-update", true),
chromedp.Flag("enable-automation", true),
chromedp.Flag("password-store", "basic"),
chromedp.Flag("use-mock-keychain", true),
)
defer pcancel()
ctx, cancel = chromedp.NewContext(
pctx,
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
} else {
ctx, cancel = chromedp.NewContext(
timeoutCtx,
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
}
defer cancel()
var htmlContent string
url := fmt.Sprintf("https://xueqiu.com/snowman/S/%s/detail#/ZYCWZB", stockCode)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
chromedp.WaitVisible("table.table", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
var messages []string
document.Find("table tr").Each(func(i int, selection *goquery.Selection) {
tr := ""
selection.Find("th,td").Each(func(i int, selection *goquery.Selection) {
ret := selection.Find("p").First().Text()
if ret == "" {
ret = selection.Text()
}
text := strutil.RemoveNonPrintable(ret)
tr += text + " "
})
logger.SugaredLogger.Infof("%s", tr+" \n")
messages = append(messages, tr+" \n")
})
return &messages
}
func GetTelegraphList(crawlTimeOut int64) *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
})
return &telegraph
}
func GetTopNewsList(crawlTimeOut int64) *[]string {
url := "https://www.cls.cn"
response, err := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.home-article-title a,div.home-article-rec a").Each(func(i int, selection *goquery.Selection) {
logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
})
return &telegraph
}
func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result, chatId, question string) {
db.Dao.Create(&models.AIResponseResult{
StockCode: stockCode,
StockName: stockName,
ModelName: o.Model,
Content: result,
ChatId: chatId,
Question: question,
})
}
func (o OpenAi) GetAIResponseResult(stock string) *models.AIResponseResult {
var result models.AIResponseResult
db.Dao.Where("stock_code = ?", stock).Order("id desc").Limit(1).First(&result)
return &result
}

View File

@@ -0,0 +1,30 @@
package data
import (
"context"
"go-stock/backend/db"
"testing"
)
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
db.Init("../../data/stock.db")
ai := NewDeepSeekOpenAi(context.TODO())
res := ai.NewChatStream("北京文化", "sz000802", "")
for {
select {
case msg := <-res:
t.Log(msg)
}
}
}
func TestGetTopNewsList(t *testing.T) {
GetTopNewsList(30)
}
func TestSearchGuShiTongStockInfo(t *testing.T) {
SearchGuShiTongStockInfo("hk01810", 60)
SearchGuShiTongStockInfo("sh600745", 60)
SearchGuShiTongStockInfo("gb_goog", 60)
}

View File

@@ -0,0 +1,120 @@
package data
import (
"encoding/json"
"go-stock/backend/db"
"go-stock/backend/logger"
"gorm.io/gorm"
)
type Settings struct {
gorm.Model
TushareToken string `json:"tushareToken"`
LocalPushEnable bool `json:"localPushEnable"`
DingPushEnable bool `json:"dingPushEnable"`
DingRobot string `json:"dingRobot"`
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
RefreshInterval int64 `json:"refreshInterval"`
OpenAiEnable bool `json:"openAiEnable"`
OpenAiBaseUrl string `json:"openAiBaseUrl"`
OpenAiApiKey string `json:"openAiApiKey"`
OpenAiModelName string `json:"openAiModelName"`
OpenAiMaxTokens int `json:"openAiMaxTokens"`
OpenAiTemperature float64 `json:"openAiTemperature"`
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
Prompt string `json:"prompt"`
CheckUpdate bool `json:"checkUpdate"`
QuestionTemplate string `json:"questionTemplate"`
CrawlTimeOut int64 `json:"crawlTimeOut"`
KDays int64 `json:"kDays"`
EnableDanmu bool `json:"enableDanmu"`
}
func (receiver Settings) TableName() string {
return "settings"
}
type SettingsApi struct {
Config Settings
}
func NewSettingsApi(settings *Settings) *SettingsApi {
return &SettingsApi{
Config: *settings,
}
}
func (s SettingsApi) UpdateConfig() string {
count := int64(0)
db.Dao.Model(s.Config).Count(&count)
if count > 0 {
db.Dao.Model(s.Config).Where("id=?", s.Config.ID).Updates(map[string]any{
"local_push_enable": s.Config.LocalPushEnable,
"ding_push_enable": s.Config.DingPushEnable,
"ding_robot": s.Config.DingRobot,
"update_basic_info_on_start": s.Config.UpdateBasicInfoOnStart,
"refresh_interval": s.Config.RefreshInterval,
"open_ai_enable": s.Config.OpenAiEnable,
"open_ai_base_url": s.Config.OpenAiBaseUrl,
"open_ai_api_key": s.Config.OpenAiApiKey,
"open_ai_model_name": s.Config.OpenAiModelName,
"open_ai_max_tokens": s.Config.OpenAiMaxTokens,
"open_ai_temperature": s.Config.OpenAiTemperature,
"tushare_token": s.Config.TushareToken,
"prompt": s.Config.Prompt,
"check_update": s.Config.CheckUpdate,
"open_ai_api_time_out": s.Config.OpenAiApiTimeOut,
"question_template": s.Config.QuestionTemplate,
"crawl_time_out": s.Config.CrawlTimeOut,
"k_days": s.Config.KDays,
"enable_danmu": s.Config.EnableDanmu,
})
} else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
db.Dao.Model(s.Config).Create(&Settings{
LocalPushEnable: s.Config.LocalPushEnable,
DingPushEnable: s.Config.DingPushEnable,
DingRobot: s.Config.DingRobot,
UpdateBasicInfoOnStart: s.Config.UpdateBasicInfoOnStart,
RefreshInterval: s.Config.RefreshInterval,
OpenAiEnable: s.Config.OpenAiEnable,
OpenAiBaseUrl: s.Config.OpenAiBaseUrl,
OpenAiApiKey: s.Config.OpenAiApiKey,
OpenAiModelName: s.Config.OpenAiModelName,
OpenAiMaxTokens: s.Config.OpenAiMaxTokens,
OpenAiTemperature: s.Config.OpenAiTemperature,
TushareToken: s.Config.TushareToken,
Prompt: s.Config.Prompt,
CheckUpdate: s.Config.CheckUpdate,
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
QuestionTemplate: s.Config.QuestionTemplate,
CrawlTimeOut: s.Config.CrawlTimeOut,
KDays: s.Config.KDays,
EnableDanmu: s.Config.EnableDanmu,
})
}
return "保存成功!"
}
func (s SettingsApi) GetConfig() *Settings {
var settings Settings
db.Dao.Model(&Settings{}).First(&settings)
if settings.OpenAiEnable {
if settings.OpenAiApiTimeOut <= 0 {
settings.OpenAiApiTimeOut = 60 * 5
}
if settings.CrawlTimeOut <= 0 {
settings.CrawlTimeOut = 60
}
if settings.KDays < 30 {
settings.KDays = 120
}
}
return &settings
}
func (s SettingsApi) Export() string {
d, _ := json.MarshalIndent(s.GetConfig(), "", " ")
return string(d)
}

View File

@@ -6,64 +6,88 @@ package data
//-----------------------------------------------------------------------------------
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil"
"github.com/duke-git/lancet/v2/validator"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"golang.org/x/sys/windows/registry"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"gorm.io/gorm"
"io/ioutil"
"gorm.io/plugin/soft_delete"
"io"
"strings"
"time"
)
// http://hq.sinajs.cn/rn=1730966120830&list=sh600000,sh600859
const sina_stook_url = "http://hq.sinajs.cn/rn=%d&list=%s"
const tushare_api_url = "http://api.tushare.pro"
const TushareToken = "9125ec636217a99a3218a64fc63507e95205f2666590792923cbaedf"
const sinaStockUrl = "http://hq.sinajs.cn/rn=%d&list=%s"
const tushareApiUrl = "http://api.tushare.pro"
type StockDataApi struct {
client *resty.Client
config *Settings
}
type StockInfo struct {
gorm.Model
Date string `json:"日期" gorm:"index"`
Time string `json:"时间" gorm:"index"`
Code string `json:"股票代码" gorm:"index"`
Name string `json:"股票名称" gorm:"index"`
Price string `json:"当前价格"`
Volume string `json:"成交的股票数"`
Amount string `json:"成交金额"`
Open string `json:"今日开盘价"`
PreClose string `json:"昨日收盘价"`
High string `json:"今日最低价"`
Low string `json:"今日最高价"`
Bid string `json:"竞买价"`
Ask string `json:"竞价"`
B1P string `json:"买一报价"`
B1V string `json:"买一报"`
B2P string `json:"买二报价"`
B2V string `json:"买二报"`
B3P string `json:"买三报价"`
B3V string `json:"买三报"`
B4P string `json:"买四报价"`
B4V string `json:"买四报"`
B5P string `json:"买五报价"`
B5V string `json:"买五报"`
A1P string `json:"卖一报价"`
A1V string `json:"卖一报"`
A2P string `json:"卖二报价"`
A2V string `json:"卖二报"`
A3P string `json:"卖三报价"`
A3V string `json:"卖三报"`
A4P string `json:"卖四报价"`
A4V string `json:"卖四报"`
A5P string `json:"卖五报价"`
A5V string `json:"卖五报"`
Date string `json:"日期" gorm:"index"`
Time string `json:"时间" gorm:"index"`
Code string `json:"股票代码" gorm:"index"`
Name string `json:"股票名称" gorm:"index"`
PrePrice float64 `json:"上次当前价格"`
Price string `json:"当前价格"`
Volume string `json:"成交的股票数"`
Amount string `json:"成交金额"`
Open string `json:"今日开盘价"`
PreClose string `json:"昨日收盘价"`
High string `json:"今日最高价"`
Low string `json:"今日最低价"`
Bid string `json:"竞价"`
Ask string `json:"竞卖价"`
B1P string `json:"买一报"`
B1V string `json:"买一申报"`
B2P string `json:"买二报"`
B2V string `json:"买二申报"`
B3P string `json:"买三报"`
B3V string `json:"买三申报"`
B4P string `json:"买四报"`
B4V string `json:"买四申报"`
B5P string `json:"买五报"`
B5V string `json:"买五申报"`
A1P string `json:"卖一报"`
A1V string `json:"卖一申报"`
A2P string `json:"卖二报"`
A2V string `json:"卖二申报"`
A3P string `json:"卖三报"`
A3V string `json:"卖三申报"`
A4P string `json:"卖四报"`
A4V string `json:"卖四申报"`
A5P string `json:"卖五报"`
A5V string `json:"卖五申报"`
//以下是字段值需二次计算
ChangePercent float64 `json:"changePercent"` //涨跌幅
ChangePrice float64 `json:"changePrice"` //涨跌额
HighRate float64 `json:"highRate"` //最高涨跌
LowRate float64 `json:"lowRate"` //最低涨跌
CostPrice float64 `json:"costPrice"` //成本价
CostVolume int64 `json:"costVolume"` //持仓数量
Profit float64 `json:"profit"` //总盈亏率
ProfitAmount float64 `json:"profitAmount"` //总盈亏金额
ProfitAmountToday float64 `json:"profitAmountToday"` //今日盈亏金额
Sort int64 `json:"sort"` //排序
AlarmChangePercent float64 `json:"alarmChangePercent"`
AlarmPrice float64 `json:"alarmPrice"`
}
func (receiver StockInfo) TableName() string {
@@ -125,15 +149,18 @@ type StockBasic struct {
}
type FollowedStock struct {
StockCode string
Name string
Volume int64
CostPrice float64
Price float64
PriceChange float64
ChangePercent float64
Time time.Time
Sort int64
StockCode string
Name string
Volume int64
CostPrice float64
Price float64
PriceChange float64
ChangePercent float64
AlarmChangePercent float64
AlarmPrice float64
Time time.Time
Sort int64
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver FollowedStock) TableName() string {
@@ -158,20 +185,69 @@ func (receiver StockBasic) TableName() string {
func NewStockDataApi() *StockDataApi {
return &StockDataApi{
client: resty.New(),
config: getConfig(),
}
}
// GetIndexBasic 获取指数信息
func (receiver StockDataApi) GetIndexBasic() {
res := &TushareStockBasicResponse{}
fields := "ts_code,name,market,publisher,category,base_date,base_point,list_date,fullname,index_type,weight_rule,desc"
_, err := receiver.client.R().
SetHeader("content-type", "application/json").
SetBody(&TushareRequest{
ApiName: "index_basic",
Token: receiver.config.TushareToken,
Params: nil,
Fields: fields}).
SetResult(res).
Post(tushareApiUrl)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
if res.Code != 0 {
logger.SugaredLogger.Error(res.Msg)
return
}
//ioutil.WriteFile("index_basic.json", resp.Body(), 0666)
for _, item := range res.Data.Items {
data := map[string]any{}
for _, field := range strings.Split(fields, ",") {
idx := slice.IndexOf(res.Data.Fields, field)
if idx == -1 {
continue
}
data[field] = item[idx]
}
index := &IndexBasic{}
jsonData, _ := json.Marshal(data)
err := json.Unmarshal(jsonData, index)
if err != nil {
continue
}
index.ID = 0
db.Dao.Model(&IndexBasic{}).FirstOrCreate(index, &IndexBasic{TsCode: index.TsCode}).Where("ts_code = ?", index.TsCode).Updates(index)
}
}
// map转换为结构体
func (receiver StockDataApi) GetStockBaseInfo() {
res := &TushareStockBasicResponse{}
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"
_, err := receiver.client.R().
SetHeader("content-type", "application/json").
SetBody(&TushareRequest{
ApiName: "stock_basic",
Token: TushareToken,
Token: receiver.config.TushareToken,
Params: nil,
Fields: "*",
Fields: fields,
}).
SetResult(res).
Post(tushare_api_url)
Post(tushareApiUrl)
//logger.SugaredLogger.Infof("GetStockBaseInfo %s", string(resp.Body()))
//resp.Body()写入文件
//ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
@@ -185,65 +261,100 @@ func (receiver StockDataApi) GetStockBaseInfo() {
return
}
for _, item := range res.Data.Items {
ID, _ := convertor.ToInt(item[6])
stock := &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(ID)
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(&StockBasic{}).FirstOrCreate(stock, &StockBasic{TsCode: stock.TsCode}).Updates(stock)
data := map[string]any{}
for _, field := range strings.Split(fields, ",") {
logger.SugaredLogger.Infof("field: %s", field)
idx := slice.IndexOf(res.Data.Fields, field)
if idx == -1 {
continue
}
data[field] = item[idx]
}
jsonData, _ := json.Marshal(data)
err := json.Unmarshal(jsonData, stock)
if err != nil {
continue
}
stock.ID = 0
db.Dao.Model(&StockBasic{}).FirstOrCreate(stock, &StockBasic{TsCode: stock.TsCode}).Where("ts_code = ?", stock.TsCode).Updates(stock)
}
}
func (receiver StockDataApi) GetStockCodeRealTimeData(StockCode string) (*StockInfo, error) {
func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]StockInfo, error) {
codes := slice.JoinFunc(StockCodes, ",", func(s string) string {
if strings.HasPrefix(s, "us") {
s = strings.Replace(s, "us", "gb_", 1)
}
if strings.HasPrefix(s, "US") {
s = strings.Replace(s, "US", "gb_", 1)
}
return strings.ToLower(s)
})
url := fmt.Sprintf(sinaStockUrl, time.Now().Unix(), codes)
//logger.SugaredLogger.Infof("GetStockCodeRealTimeData %s", url)
resp, err := receiver.client.R().
SetHeader("Host", "hq.sinajs.cn").
SetHeader("Referer", "https://finance.sina.com.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
Get(fmt.Sprintf(sina_stook_url, time.Now().Unix(), StockCode))
Get(url)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &StockInfo{}, nil
return &[]StockInfo{}, err
}
stockData, err := ParseFullSingleStockData(GB18030ToUTF8(resp.Body()))
var count int64
db.Dao.Model(&StockInfo{}).Where("code = ?", StockCode).Count(&count)
if count == 0 {
go db.Dao.Model(&StockInfo{}).Create(stockData)
} else {
go db.Dao.Model(&StockInfo{}).Where("code = ?", StockCode).Updates(stockData)
stockInfos := make([]StockInfo, 0)
str := GB18030ToUTF8(resp.Body())
dataStr := strutil.SplitEx(str, "\n", true)
if len(dataStr) == 0 {
return &[]StockInfo{}, errors.New("获取股票信息失败,请检查股票代码是否正确")
}
return stockData, err
for _, data := range dataStr {
//logger.SugaredLogger.Info(data)
stockData, err := ParseFullSingleStockData(data)
if err != nil {
logger.SugaredLogger.Error(err.Error())
continue
}
stockInfos = append(stockInfos, *stockData)
go func() {
var count int64
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Count(&count)
if count == 0 {
db.Dao.Model(&StockInfo{}).Create(stockData)
} else {
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Updates(stockData)
}
}()
}
return &stockInfos, err
}
func (receiver StockDataApi) Follow(stockCode string) string {
stockInfo, err := receiver.GetStockCodeRealTimeData(stockCode)
if err != nil {
logger.SugaredLogger.Error(err.Error())
logger.SugaredLogger.Infof("Follow %s", stockCode)
stockInfos, err := receiver.GetStockCodeRealTimeData(stockCode)
if err != nil || len(*stockInfos) == 0 {
logger.SugaredLogger.Error(err)
return "关注失败"
}
stockInfo := (*stockInfos)[0]
price, _ := convertor.ToFloat(stockInfo.Price)
db.Dao.Model(&FollowedStock{}).FirstOrCreate(&FollowedStock{
StockCode: stockCode,
Name: stockInfo.Name,
Price: price,
Time: time.Now(),
ChangePercent: 0,
PriceChange: 0,
StockCode: stockCode,
Name: stockInfo.Name,
Price: price,
Time: time.Now(),
ChangePercent: 0,
PriceChange: 0,
Sort: 0,
AlarmChangePercent: 3,
AlarmPrice: price + 1,
}, &FollowedStock{StockCode: stockCode})
return "关注成功"
}
@@ -262,6 +373,22 @@ func (receiver StockDataApi) SetCostPriceAndVolume(price float64, volume int64,
return "设置成功"
}
func (receiver StockDataApi) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", stockCode).Updates(&map[string]any{
"alarm_change_percent": val,
"alarm_price": alarmPrice,
}).Error
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "设置失败"
}
return "设置成功"
}
func (receiver StockDataApi) SetStockSort(sort int64, stockCode string) {
db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", stockCode).Update("sort", sort)
}
func (receiver StockDataApi) GetFollowList() []FollowedStock {
var result []FollowedStock
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result)
@@ -271,13 +398,49 @@ func (receiver StockDataApi) GetFollowList() []FollowedStock {
func (receiver StockDataApi) GetStockList(key string) []StockBasic {
var result []StockBasic
db.Dao.Model(&StockBasic{}).Where("name like ? or ts_code like ?", "%"+key+"%", "%"+key+"%").Find(&result)
var result2 []IndexBasic
db.Dao.Model(&IndexBasic{}).Where("market in ?", []string{"SSE", "SZSE"}).Where("name like ? or ts_code like ?", "%"+key+"%", "%"+key+"%").Find(&result2)
var result3 []models.StockInfoHK
db.Dao.Model(&models.StockInfoHK{}).Where("name like ? or code like ?", "%"+key+"%", "%"+key+"%").Find(&result3)
var result4 []models.StockInfoUS
db.Dao.Model(&models.StockInfoUS{}).Where("name like ? or code like ?", "%"+key+"%", "%"+key+"%").Find(&result4)
for _, item := range result2 {
result = append(result, StockBasic{
TsCode: item.TsCode,
Name: item.Name,
Fullname: item.FullName,
Symbol: item.Symbol,
Market: item.Market,
ListDate: item.ListDate,
})
}
for _, item := range result3 {
result = append(result, StockBasic{
TsCode: item.Code,
Name: item.Name,
Fullname: item.Name,
Market: "HK",
})
}
for _, item := range result4 {
result = append(result, StockBasic{
TsCode: item.Code,
Name: item.Name,
Fullname: item.Name,
Market: "US",
})
}
return result
}
// GB18030 转换为 UTF8
// GB18030ToUTF8 GB18030 转换为 UTF8
func GB18030ToUTF8(bs []byte) string {
reader := transform.NewReader(bytes.NewReader(bs), simplifiedchinese.GB18030.NewDecoder())
d, err := ioutil.ReadAll(reader)
d, err := io.ReadAll(reader)
if err != nil {
panic(err)
}
@@ -289,6 +452,135 @@ func ParseFullSingleStockData(data string) (*StockInfo, error) {
if len(datas) < 2 {
return nil, fmt.Errorf("invalid data format")
}
var result map[string]string
var err error
if strutil.ContainsAny(datas[0], []string{"hq_str_sz", "hq_str_sh"}) {
result, err = ParseSHSZStockData(datas)
}
if strutil.ContainsAny(datas[0], []string{"hq_str_hk"}) {
result, err = ParseHKStockData(datas)
}
if strutil.ContainsAny(datas[0], []string{"hq_str_gb"}) {
result, err = ParseUSStockData(datas)
}
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
marshal, err := json.Marshal(result)
if err != nil {
return nil, err
}
stockInfo := &StockInfo{}
err = json.Unmarshal(marshal, &stockInfo)
if err != nil {
return nil, err
}
//logger.SugaredLogger.Infof("股票数据解析完成stockInfo: %+v", stockInfo)
return stockInfo, nil
}
func ParseUSStockData(datas []string) (map[string]string, error) {
code := strings.Split(datas[0], "hq_str_")[1]
result := make(map[string]string)
parts := strutil.SplitAndTrim(datas[1], ",", "\"", ";")
//parts := strings.Split(data, ",")
if len(parts) < 30 {
return nil, fmt.Errorf("invalid data format")
}
/*
谷歌, 0
170.2100, 1 现价
-2.57, 2 涨跌幅
2025-02-28 09:38:50, 3 时间
-4.4900, 4 涨跌额
175.9400, 5 今日开盘价
176.5900, 6 区间
169.7520, 7 区间
208.7000, 8 52周区间
130.9500, 9 52周区间
25930485, 10 成交量
17083496, 11 10日均量
2074859900000, 12 市值
8.13, 13 每股收益
20.940000 , 14 市盈率
0.00, 15
0.00, 16
0.20, 17
0.00, 18
12190000000, 19
71, 20
170.2000, 21 盘后
-0.01, 22
-0.01, 23
Feb 27 07:59PM EST, 24
Feb 27 04:00PM EST, 25
174.7000, 26 前收盘
2917444, 27
1, 28
2025, 29
4456143849.0000, 30
176.1200, 31
163.7039, 32
496605933.1411, 33
170.2100, 34 现价
174.7000 35 前收盘
*/
result["股票代码"] = code
result["股票名称"] = parts[0]
result["今日开盘价"] = parts[5]
result["昨日收盘价"] = parts[26]
result["今日最高价"] = parts[6]
result["今日最低价"] = parts[7]
result["当前价格"] = parts[1]
result["日期"] = strutil.SplitAndTrim(parts[3], " ", "")[0]
result["时间"] = strutil.SplitAndTrim(parts[3], " ", "")[1]
//logger.SugaredLogger.Infof("美股股票数据解析完成: %v", result)
return result, nil
}
func ParseHKStockData(datas []string) (map[string]string, error) {
code := strings.Split(datas[0], "hq_str_")[1]
result := make(map[string]string)
parts := strutil.SplitAndTrim(datas[1], ",", "\"", ";")
//parts := strings.Split(data, ",")
if len(parts) < 19 {
return nil, fmt.Errorf("invalid data format")
}
/*
XIAOMI-W, 0
小米集团-W, 1 股票名称
50.050, 2 今日开盘价
49.150, 3 昨日收盘价
51.950, 4 今日最高价
49.700, 5 今日最低价
51.700, 6 当前价格
2.550, 7 涨跌额
5.188, 8 涨跌幅
51.65000, 9
51.70000, 10
15770408249, 11 成交额
308362585, 12 成交量
0.000, 13
0.000, 14
51.950, 15 52周最高
12.560, 16 52周最低
2025/02/21, 17
16:08 18
*/
result["股票代码"] = code
result["股票名称"] = parts[1]
result["今日开盘价"] = parts[2]
result["昨日收盘价"] = parts[3]
result["今日最高价"] = parts[4]
result["今日最低价"] = parts[5]
result["当前价格"] = parts[6]
result["日期"] = strings.ReplaceAll(parts[17], "/", "-")
result["时间"] = strings.ReplaceAll(parts[18], "\";", ":00")
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
return result, nil
}
func ParseSHSZStockData(datas []string) (map[string]string, error) {
code := strings.Split(datas[0], "hq_str_")[1]
result := make(map[string]string)
parts := strutil.SplitAndTrim(datas[1], ",", "\"")
@@ -355,17 +647,385 @@ func ParseFullSingleStockData(data string) (*StockInfo, error) {
result["卖五报价"] = parts[29]
result["日期"] = parts[30]
result["时间"] = parts[31]
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
marshal, err := json.Marshal(result)
if err != nil {
return nil, err
}
stockInfo := &StockInfo{}
err = json.Unmarshal(marshal, &stockInfo)
if err != nil {
return nil, err
}
//logger.SugaredLogger.Infof("股票数据解析完成stockInfo: %+v", stockInfo)
return stockInfo, nil
return result, nil
}
type IndexBasic struct {
gorm.Model
TsCode string `json:"ts_code" gorm:"index"`
Symbol string `json:"symbol" gorm:"index"`
Name string `json:"name" gorm:"index"`
FullName string `json:"fullname"`
IndexType string `json:"index_type"`
IndexCategory string `json:"category"`
Market string `json:"market"`
ListDate string `json:"list_date"`
BaseDate string `json:"base_date"`
BasePoint float64 `json:"base_point"`
Publisher string `json:"publisher"`
WeightRule string `json:"weight_rule"`
DESC string `json:"desc"`
}
func (IndexBasic) TableName() string {
return "tushare_index_basic"
}
type RealTimeStockPriceInfo struct {
StockCode string
Price string `json:"当前价格"`
Time time.Time
}
func GetRealTimeStockPriceInfo(ctx context.Context, stockCode string) (price, priceTime string) {
if strutil.HasPrefixAny(stockCode, []string{"SZ", "SH", "sh", "sz"}) {
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "EastmoneyCrawler",
Description: "EastmoneyCrawler 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"},
}
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
htmlContent, ok := crawlerAPI.GetHtml(fmt.Sprintf("https://quote.eastmoney.com/%s.html", stockCode), "div.zxj", true)
if ok {
price := ""
priceTime := ""
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Errorf("GetRealTimeStockPriceInfo error: %v", err)
}
document.Find("div.zxj").Each(func(i int, selection *goquery.Selection) {
price = selection.Text()
logger.SugaredLogger.Infof("股票代码: %s, 当前价格: %s", stockCode, price)
})
document.Find("span.quote_title_time").Each(func(i int, selection *goquery.Selection) {
priceTime = selection.Text()
logger.SugaredLogger.Infof("股票代码: %s, 当前价格时间: %s", stockCode, priceTime)
})
return price, priceTime
}
}
return price, priceTime
}
func SearchStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
if strutil.HasPrefixAny(stockCode, []string{"SZ", "SH", "sh", "sz"}) {
return getSHSZStockPriceInfo(stockCode, crawlTimeOut)
}
if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) {
return getHKStockPriceInfo(stockCode, crawlTimeOut)
}
if strutil.HasPrefixAny(stockCode, []string{"US", "us", "gb_"}) {
return getUSStockPriceInfo(stockCode, crawlTimeOut)
}
return &[]string{}
}
func getUSStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
var messages []string
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "SinaCrawler",
Description: "SinaCrawler 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(), time.Duration(crawlTimeOut)*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", strings.ReplaceAll(stockCode, "gb_", ""))
htmlContent, ok := crawlerAPI.GetHtml(url, "div#hqPrice", true)
if !ok {
return &[]string{}
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
stockName := ""
stockPrice := ""
stockPriceTime := ""
document.Find("div.hq_title >h1").Each(func(i int, selection *goquery.Selection) {
stockName = strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("股票名称-:%s", stockName)
})
document.Find("#hqPrice").Each(func(i int, selection *goquery.Selection) {
stockPrice = strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("现价: %s", stockPrice)
})
document.Find("div.hq_time").Each(func(i int, selection *goquery.Selection) {
stockPriceTime = strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("时间: %s", stockPriceTime)
})
messages = append(messages, fmt.Sprintf("%s:%s现价%s", stockPriceTime, stockName, stockPrice))
logger.SugaredLogger.Infof("股票: %s", messages)
document.Find("div#hqDetails >table tbody tr").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("股票名称-%s: %s", stockName, text)
messages = append(messages, text)
})
return &messages
}
func getHKStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
var messages []string
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "SinaCrawler",
Description: "SinaCrawler 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(), time.Duration(crawlTimeOut)*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
url := fmt.Sprintf("https://stock.finance.sina.com.cn/hkstock/quotes/%s.html", strings.ReplaceAll(stockCode, "hk", ""))
htmlContent, ok := crawlerAPI.GetHtml(url, ".deta_hqContainer >.deta03 ", true)
if !ok {
return &[]string{}
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
stockName := ""
stockPrice := ""
stockPriceTime := ""
document.Find("#stock_cname").Each(func(i int, selection *goquery.Selection) {
stockName = strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("股票名称-:%s", stockName)
})
document.Find("#mts_stock_hk_price").Each(func(i int, selection *goquery.Selection) {
stockPrice = strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("现价: %s", stockPrice)
})
document.Find("#mts_stock_hk_time").Each(func(i int, selection *goquery.Selection) {
stockPriceTime = strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("时间: %s", stockPriceTime)
})
messages = append(messages, fmt.Sprintf("%s:%s现价%s", stockPriceTime, stockName, stockPrice))
logger.SugaredLogger.Infof("股票: %s", messages)
document.Find(".deta_hqContainer >.deta03 li").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("股票名称-%s: %s", stockName, text)
messages = append(messages, text)
})
return &messages
}
func getSHSZStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
var messages []string
url := "https://www.cls.cn/stock?code=" + stockCode
// 创建一个 chromedp 上下文
timeoutCtx, timeoutCtxCancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
defer timeoutCtxCancel()
var ctx context.Context
var cancel context.CancelFunc
path, e := checkBrowserOnWindows()
logger.SugaredLogger.Infof("SearchStockPriceInfo path:%s", path)
if e {
pctx, pcancel := chromedp.NewExecAllocator(
timeoutCtx,
chromedp.ExecPath(path),
chromedp.Flag("headless", true),
)
defer pcancel()
ctx, cancel = chromedp.NewContext(
pctx,
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
} else {
ctx, cancel = chromedp.NewContext(
timeoutCtx,
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
}
defer cancel()
var htmlContent string
var tasks chromedp.Tasks
tasks = append(tasks, chromedp.Navigate(url))
tasks = append(tasks, chromedp.WaitVisible("div.quote-change-box", chromedp.ByQuery))
tasks = append(tasks, chromedp.ActionFunc(func(ctx context.Context) error {
price, _ := FetchPrice(ctx)
logger.SugaredLogger.Infof("price:%s", price)
return nil
}))
tasks = append(tasks, chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery))
err := chromedp.Run(ctx, tasks)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
document.Find("div.quote-text-border,span.quote-price").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Info(text)
messages = append(messages, text)
})
return &messages
}
func FetchPrice(ctx context.Context) (string, error) {
var price string
timeout := time.After(10 * time.Second) // 设置超时时间为10秒
ticker := time.NewTicker(1 * time.Second) // 每秒尝试一次
defer ticker.Stop()
for {
select {
case <-timeout:
return "", fmt.Errorf("timeout reached while fetching price")
case <-ticker.C:
err := chromedp.Run(ctx, chromedp.Text("span.quote-price", &price, chromedp.BySearch))
if err != nil {
logger.SugaredLogger.Errorf("failed to fetch price: %v", err)
continue
}
logger.SugaredLogger.Infof("price:%s", price)
if price != "" && validator.IsNumberStr(price) {
return price, nil
}
}
}
}
func SearchStockInfo(stock, msgType string, crawlTimeOut int64) *[]string {
crawler := CrawlerApi{
crawlerBaseInfo: CrawlerBaseInfo{
Name: "财联社",
BaseUrl: "https://www.cls.cn",
Description: "财联社",
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"},
},
}
timeoutCtx, timeoutCtxCancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
defer timeoutCtxCancel()
crawler = crawler.NewCrawler(timeoutCtx, crawler.crawlerBaseInfo)
url := fmt.Sprintf("https://www.cls.cn/searchPage?keyword=%s&type=%s", RemoveAllBlankChar(stock), msgType)
logger.SugaredLogger.Infof("SearchStockInfo url:%s", url)
waitVisible := ".search-telegraph-list,.subject-interest-list"
htmlContent, ok := crawler.GetHtml(url, waitVisible, true)
if !ok {
return &[]string{}
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
var messages []string
document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
messages = append(messages, ReplaceSensitiveWords(text))
logger.SugaredLogger.Infof("搜索到消息-%s: %s", msgType, text)
})
return &messages
}
func SearchStockInfoByCode(stock string) *[]string {
// 创建一个 chromedp 上下文
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
defer cancel()
var htmlContent string
stock = strings.ReplaceAll(stock, "sh", "")
stock = strings.ReplaceAll(stock, "sz", "")
url := fmt.Sprintf("https://gushitong.baidu.com/stock/ab-%s", stock)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
//chromedp.Sleep(3*time.Second),
chromedp.WaitVisible("a.news-item-link", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
var messages []string
document.Find("a.news-item-link").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
if strings.Contains(text, stock) {
messages = append(messages, text)
logger.SugaredLogger.Infof("搜索到消息: %s", text)
}
})
return &messages
}
// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装
func checkChromeOnWindows() (string, 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()
path, _, err := key.GetStringValue("Path")
logger.SugaredLogger.Infof("Chrome安装路径%s", path)
if err != nil {
return "", false
}
return path + "\\chrome.exe", true
}
// checkBrowserOnWindows 在 Windows 系统上检查Edge浏览器是否安装并返回安装路径
func checkBrowserOnWindows() (string, bool) {
if path, ok := checkChromeOnWindows(); ok {
return path, true
}
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")
logger.SugaredLogger.Infof("Edge安装路径%s", path)
if err != nil {
return "", false
}
return path + "\\msedge.exe", true
}

View File

@@ -1,12 +1,19 @@
package data
import (
"context"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
"io/ioutil"
"regexp"
"strings"
"testing"
"time"
)
// @Author spark
@@ -14,9 +21,93 @@ import (
// @Desc
//-----------------------------------------------------------------------------------
func TestGetTelegraph(t *testing.T) {
GetTelegraphList(30)
}
func TestGetFinancialReports(t *testing.T) {
//GetFinancialReports("sz000802", 30)
//GetFinancialReports("hk00927", 30)
GetFinancialReports("gb_aapl", 30)
}
func TestGetTelegraphSearch(t *testing.T) {
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
messages := SearchStockInfo("谷歌", "telegram", 30)
for _, message := range *messages {
logger.SugaredLogger.Info(message)
}
//https://www.cls.cn/stock?code=sh600745
}
func TestSearchStockInfoByCode(t *testing.T) {
SearchStockInfoByCode("sh600745")
}
func TestSearchStockPriceInfo(t *testing.T) {
//SearchStockPriceInfo("hk06030", 30)
//SearchStockPriceInfo("sh600171", 30)
SearchStockPriceInfo("gb_aapl", 30)
}
func TestGetRealTimeStockPriceInfo(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
text, texttime := GetRealTimeStockPriceInfo(ctx, "sh600171")
logger.SugaredLogger.Infof("res:%s,%s", text, texttime)
text, texttime = GetRealTimeStockPriceInfo(ctx, "sh600438")
logger.SugaredLogger.Infof("res:%s,%s", text, texttime)
texttime = strings.ReplaceAll(texttime, "", "")
texttime = strings.ReplaceAll(texttime, "", "")
parts := strings.Split(texttime, " ")
logger.SugaredLogger.Infof("parts:%+v", parts)
//去除中文字符
// 正则表达式匹配中文字符
re := regexp.MustCompile(`\p{Han}+`)
texttime = re.ReplaceAllString(texttime, "")
logger.SugaredLogger.Infof("texttime:%s", texttime)
location, err := time.ParseInLocation("2006-01-02 15:04:05", texttime, time.Local)
if err != nil {
return
}
logger.SugaredLogger.Infof("location:%s", location.Format("2006-01-02 15:04:05"))
}
func TestParseFullSingleStockData(t *testing.T) {
resp, err := resty.New().R().
SetHeader("Host", "hq.sinajs.cn").
SetHeader("Referer", "https://finance.sina.com.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600584,sz000938,hk01810,hk00856,gb_aapl"))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
data := GB18030ToUTF8(resp.Body())
strs := strutil.SplitEx(data, "\n", true)
for _, str := range strs {
logger.SugaredLogger.Info(str)
stockData, err := ParseFullSingleStockData(str)
if err != nil {
return
}
logger.SugaredLogger.Infof("%+#v", stockData)
}
}
func TestNewStockDataApi(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
t.Log(stockDataApi.GetStockCodeRealTimeData("sh600859"))
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745")
for _, data := range *datas {
t.Log(data)
}
}
func TestGetStockBaseInfo(t *testing.T) {
@@ -74,6 +165,12 @@ func TestReadFile(t *testing.T) {
func TestFollowedList(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
t.Log(stockDataApi.GetFollowList())
stockDataApi.GetFollowList()
}
func TestStockDataApi_GetIndexBasic(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
stockDataApi.GetIndexBasic()
}

View File

@@ -0,0 +1,93 @@
package data
import (
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/logger"
"strings"
"time"
)
// @Author spark
// @Date 2025/2/17 12:33
// @Desc
//-----------------------------------------------------------------------------------
type TushareApi struct {
client *resty.Client
config *Settings
}
func NewTushareApi(config *Settings) *TushareApi {
return &TushareApi{
client: resty.New(),
config: config,
}
}
// GetDaily tushare A股日线行情
func (receiver TushareApi) GetDaily(tsCode, startDate, endDate string, crawlTimeOut int64) string {
logger.SugaredLogger.Debugf("tushare daily request: ts_code=%s, start_date=%s, end_date=%s", tsCode, startDate, endDate)
fields := "ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount"
resp := &TushareStockBasicResponse{}
stockType := getStockType(tsCode)
tsCodeNEW := getTsCode(tsCode)
logger.SugaredLogger.Debugf("tushare daily request: %s,tsCode:%s,tsCodeNEW:%s", stockType, tsCode, tsCodeNEW)
_, err := receiver.client.SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("content-type", "application/json").
SetBody(&TushareRequest{
ApiName: stockType,
Token: receiver.config.TushareToken,
Params: map[string]any{
"ts_code": tsCodeNEW,
"start_date": startDate,
"end_date": endDate,
},
Fields: fields}).
SetResult(resp).
Post(tushareApiUrl)
if err != nil {
logger.SugaredLogger.Error(err)
return ""
}
res := ""
if resp.Data.Items != nil && len(resp.Data.Items) > 0 {
fieldsStr := slice.JoinFunc(resp.Data.Fields, ",", func(s string) string {
return "\"" + convertor.ToString(s) + "\""
})
res += fieldsStr + "\n"
for _, item := range resp.Data.Items {
//logger.SugaredLogger.Debugf("%s", slice.Join(item, ","))
t := slice.JoinFunc(item, ",", func(s any) any {
return "\"" + convertor.ToString(s) + "\""
})
res += t + "\n"
}
}
logger.SugaredLogger.Debugf("tushare response: %s", res)
return res
}
func getTsCode(code string) any {
if strutil.HasPrefixAny(code, []string{"US", "us", "gb_"}) {
code = strings.Replace(code, "gb_", "", 1)
code = strings.Replace(code, "us", "", 1)
return code
}
return code
}
func getStockType(code string) string {
if strutil.HasSuffixAny(code, []string{"SZ", "SH", "sh", "sz"}) {
return "daily"
}
if strutil.HasSuffixAny(code, []string{"HK", "hk"}) {
return "hk_daily"
}
if strutil.HasPrefixAny(code, []string{"US", "us", "gb_"}) {
return "us_daily"
}
return ""
}

View File

@@ -0,0 +1,29 @@
package data
import (
"go-stock/backend/db"
"testing"
)
// @Author spark
// @Date 2025/2/17 12:44
// @Desc
// -----------------------------------------------------------------------------------
func TestGetDaily(t *testing.T) {
db.Init("../../data/stock.db")
tushareApi := NewTushareApi(getConfig())
res := tushareApi.GetDaily("00927.HK", "20250101", "20250217", 30)
t.Log(res)
}
func TestGetUSDaily(t *testing.T) {
db.Init("../../data/stock.db")
tushareApi := NewTushareApi(getConfig())
res := tushareApi.GetDaily("gb_AAPL", "20250101", "20250217", 30)
t.Log(res)
//
}

59
backend/data/utils.go Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,43 @@
package data
import (
"go-stock/backend/logger"
"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)
}

197
backend/models/models.go Normal file
View File

@@ -0,0 +1,197 @@
package models
import (
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
"time"
)
// @Author spark
// @Date 2025/2/6 15:25
// @Desc
//-----------------------------------------------------------------------------------
type GitHubReleaseVersion struct {
Url string `json:"url"`
AssetsUrl string `json:"assets_url"`
UploadUrl string `json:"upload_url"`
HtmlUrl string `json:"html_url"`
Id int `json:"id"`
Author struct {
Login string `json:"login"`
Id int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
UserViewType string `json:"user_view_type"`
SiteAdmin bool `json:"site_admin"`
} `json:"author"`
NodeId string `json:"node_id"`
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish"`
Name string `json:"name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
Assets []struct {
Url string `json:"url"`
Id int `json:"id"`
NodeId string `json:"node_id"`
Name string `json:"name"`
Label string `json:"label"`
Uploader struct {
Login string `json:"login"`
Id int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
UserViewType string `json:"user_view_type"`
SiteAdmin bool `json:"site_admin"`
} `json:"uploader"`
ContentType string `json:"content_type"`
State string `json:"state"`
Size int `json:"size"`
DownloadCount int `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
BrowserDownloadUrl string `json:"browser_download_url"`
} `json:"assets"`
TarballUrl string `json:"tarball_url"`
ZipballUrl string `json:"zipball_url"`
Body string `json:"body"`
Tag Tag `json:"tag"`
Commit Commit `json:"commit"`
}
type Tag struct {
Ref string `json:"ref"`
NodeId string `json:"node_id"`
Url string `json:"url"`
Object struct {
Sha string `json:"sha"`
Type string `json:"type"`
Url string `json:"url"`
} `json:"object"`
}
type Commit struct {
Sha string `json:"sha"`
NodeId string `json:"node_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
} `json:"author"`
Committer struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
} `json:"committer"`
Tree struct {
Sha string `json:"sha"`
Url string `json:"url"`
} `json:"tree"`
Message string `json:"message"`
Parents []struct {
Sha string `json:"sha"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
} `json:"parents"`
Verification struct {
Verified bool `json:"verified"`
Reason string `json:"reason"`
Signature interface{} `json:"signature"`
Payload interface{} `json:"payload"`
VerifiedAt interface{} `json:"verified_at"`
} `json:"verification"`
}
type AIResponseResult struct {
gorm.Model
ChatId string `json:"chatId"`
ModelName string `json:"modelName"`
StockCode string `json:"stockCode"`
StockName string `json:"stockName"`
Question string `json:"question"`
Content string `json:"content"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver AIResponseResult) TableName() string {
return "ai_response_result"
}
type VersionInfo struct {
gorm.Model
Version string `json:"version"`
Content string `json:"content"`
Icon string `json:"icon"`
Alipay string `json:"alipay"`
Wxpay string `json:"wxpay"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver VersionInfo) TableName() string {
return "version_info"
}
type StockInfoHK struct {
gorm.Model
Code string `json:"code"`
Name string `json:"name"`
FullName string `json:"fullName"`
EName string `json:"eName"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver StockInfoHK) TableName() string {
return "stock_base_info_hk"
}
type StockInfoUS struct {
gorm.Model
Code string `json:"code"`
Name string `json:"name"`
FullName string `json:"fullName"`
EName string `json:"eName"`
Exchange string `json:"exchange"`
Type string `json:"type"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver StockInfoUS) TableName() string {
return "stock_base_info_us"
}
type Resp struct {
Code int `json:"code"`
Message string `json:"message"`
}

View File

@@ -0,0 +1,49 @@
package models
import (
"encoding/json"
"github.com/duke-git/lancet/v2/strutil"
"go-stock/backend/db"
"go-stock/backend/logger"
"os"
"testing"
)
// @Author spark
// @Date 2025/2/22 16:09
// @Desc
// -----------------------------------------------------------------------------------
type StockInfoHKResp struct {
Code int `json:"code"`
Status string `json:"status"`
StockInfos *[]StockInfoData `json:"data"`
}
type StockInfoData struct {
C string `json:"c"`
N string `json:"n"`
T string `json:"t"`
E string `json:"e"`
}
func TestStockInfoHK(t *testing.T) {
db.Init("../../data/stock.db")
db.Dao.AutoMigrate(&StockInfoHK{})
bs, _ := os.ReadFile("../../build/hk.json")
v := &StockInfoHKResp{}
err := json.Unmarshal(bs, v)
if err != nil {
return
}
hks := &[]StockInfoHK{}
for i, data := range *v.StockInfos {
logger.SugaredLogger.Infof("第%d条数据: %+v", i, data)
hk := &StockInfoHK{
Code: strutil.PadStart(data.C, 5, "0") + ".HK",
EName: data.N,
}
*hks = append(*hks, *hk)
}
db.Dao.Create(&hks)
}

BIN
build/app.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

15192
build/hk.json Normal file

File diff suppressed because it is too large Load Diff

BIN
build/screenshot/alipay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
build/screenshot/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
build/screenshot/img_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
build/screenshot/img_10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
build/screenshot/img_11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
build/screenshot/img_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
build/screenshot/img_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
build/screenshot/img_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
build/screenshot/img_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
build/screenshot/img_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
build/screenshot/img_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
build/screenshot/img_8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
build/screenshot/img_9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
build/screenshot/wxpay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

27843
build/stock_base_info_hk.json Normal file

File diff suppressed because it is too large Load Diff

36493
build/stock_base_info_us.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,19 +5,19 @@
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "{{.Name}}"
!define INFO_PROJECTNAME "go-stock"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
!define INFO_COMPANYNAME "sparkmemory"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
!define INFO_PRODUCTNAME "go-stock"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
!define INFO_PRODUCTVERSION "1.0.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
!define INFO_COPYRIGHT "Copyright#sparkmemory@163.com"
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
@@ -203,20 +203,12 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!macro wails.associateFiles
; Create file associations
{{range .Info.FileAssociations}}
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
File "..\{{.IconName}}.ico"
{{end}}
!macroend
!macro wails.unassociateFiles
; Delete app associations
{{range .Info.FileAssociations}}
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
Delete "$INSTDIR\{{.IconName}}.ico"
{{end}}
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
@@ -235,15 +227,10 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!macro wails.associateCustomProtocols
; Create custom protocols associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
{{end}}
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
{{end}}
!macroend

BIN
build/wx.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -9,14 +9,27 @@
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.25"
"@types/file-saver": "^2.0.7",
"@vavt/cm-extension": "^1.8.0",
"@vavt/v3-extension": "^3.0.0",
"@vicons/ionicons5": "^0.13.0",
"file-saver": "^2.0.5",
"html2canvas": "^1.4.1",
"md-editor-v3": "^5.2.3",
"vue": "^3.2.25",
"vue-router": "^4.5.0",
"vue3-danmaku": "^1.6.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"naive-ui": "^2.40.3",
"html-docx-js-typescript": "^0.1.5",
"naive-ui": "^2.41.0",
"vfonts": "^0.0.3",
"vite": "5.4.6"
"vite": "^5.4.12"
},
"keywords": [],
"keywords": [
"AI赋能股票分析",
"go-stock"
],
"author": "spark"
}

View File

@@ -1 +1 @@
9ce62efac1fed08499bbf20c8a5fd1b2
2091cce83d29f564a50e85f1667b2f4c

View File

@@ -1,32 +1,251 @@
<script setup>
import stockInfo from './components/stock.vue'
import {ref} from "vue";
import {
EventsEmit,
EventsOn,
Quit,
WindowFullscreen, WindowGetPosition,
WindowHide,
WindowSetPosition,
WindowUnfullscreen
} from '../wailsjs/runtime'
import {h, ref} from "vue";
import { RouterLink } from 'vue-router'
import {darkTheme, NGradientText, NIcon, NText,} from 'naive-ui'
import {
SettingsOutline,
ReorderTwoOutline,
ExpandOutline,
PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline, AlarmOutline, SparklesOutline,
} from '@vicons/ionicons5'
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
const isFullscreen = ref(false)
const activeKey = ref('stock')
const containerRef= ref({})
const realtimeProfit= ref(0)
const telegraph= ref([])
const menuOptions = ref([
{
label: () =>
h(
RouterLink,
{
to: {
name: 'stock',
params: {
id: 'zh-CN'
},
}
},
{ default: () => '股票自选',}
),
key: 'stock',
icon: renderIcon(StarOutline),
children:[
{
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '当日盈亏 '+realtimeProfit.value+"¥"}),
key: 'realtimeProfit',
show: realtimeProfit.value,
icon: renderIcon(WalletOutline),
},
]
},
{
label: () =>
h(
NGradientText,
{
type: 'warning',
style: {
'text-decoration': 'line-through',
}
},
{ default: () => '基金自选' }
),
key: 'fund',
icon: renderIcon(SparklesOutline),
children:[
{
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '敬请期待!'}),
key: 'realtimeProfit',
show: realtimeProfit.value,
icon: renderIcon(AlarmOutline),
},
]
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'settings',
params: {
id: 'zh-CN'
}
}
},
{ default: () => '设置' }
),
key: 'settings',
icon: renderIcon(SettingsOutline),
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'about',
params: {
id: 'zh-CN'
}
}
},
{ default: () => '关于' }
),
key: 'about',
icon: renderIcon(LogoGithub),
},
{
label: ()=> h("a", {
href: '#',
onClick: toggleFullscreen,
title: '全屏 Ctrl+F 退出全屏 Esc',
}, { default: () => isFullscreen.value?'取消全屏':'全屏' }),
key: 'full',
icon: renderIcon(ExpandOutline),
},
{
label: ()=> h("a", {
href: '#',
onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+H',
}, { default: () => '隐藏到托盘区' }),
key: 'hide',
icon: renderIcon(ReorderTwoOutline),
},
{
label: ()=> h("a", {
href: 'javascript:void(0)',
style: 'cursor: move;',
onClick: toggleStartMoveWindow,
}, { default: () => '移动' }),
key: 'move',
icon: renderIcon(MoveOutline),
},
{
label: ()=> h("a", {
href: '#',
onClick: Quit,
}, { default: () => '退出程序' }),
key: 'exit',
icon: renderIcon(PowerOutline),
},
])
function renderIcon(icon) {
return () => h(NIcon, null, { default: () => h(icon) })
}
function toggleFullscreen(e) {
//console.log(e)
if (isFullscreen.value) {
WindowUnfullscreen()
//e.target.innerHTML = '全屏'
} else {
WindowFullscreen()
// e.target.innerHTML = '取消全屏'
}
isFullscreen.value=!isFullscreen.value
}
const drag = ref(false)
const lastPos= ref({x:0,y:0})
function toggleStartMoveWindow(e) {
drag.value=!drag.value
lastPos.value={x:e.clientX,y:e.clientY}
}
function dragstart(e) {
if (drag.value) {
let x=e.clientX-lastPos.value.x
let y=e.clientY-lastPos.value.y
WindowGetPosition().then((pos) => {
WindowSetPosition(pos.x+x,pos.y+y)
})
}
}
window.addEventListener('mousemove', dragstart)
const content = ref('数据来源于网络,仅供参考\n投资有风险,入市需谨慎')
EventsOn("realtime_profit",(data)=>{
realtimeProfit.value=data
})
EventsOn("telegraph",(data)=>{
telegraph.value=data
})
window.onerror = function (msg, source, lineno, colno, error) {
// 将错误信息发送给后端
EventsEmit("frontendError", {
page: "App.vue",
message: msg,
source: source,
lineno: lineno,
colno: colno,
error: error ? error.stack : null,
});
return true;
};
</script>
<template>
<n-watermark
<n-config-provider :theme="darkTheme" ref="containerRef">
<n-message-provider >
<n-notification-provider>
<n-modal-provider>
<n-dialog-provider>
<n-watermark
:content="content"
cross
selectable
:font-size="12"
:line-height="12"
:font-size="16"
:line-height="16"
:width="500"
:height="400"
:width="200"
:x-offset="50"
:y-offset="50"
:y-offset="150"
:rotate="-15"
style="height: 100%"
>
<n-flex justify="center">
<n-message-provider >
<n-modal-provider>
<stockInfo/>
</n-modal-provider>
</n-message-provider>
<n-grid x-gap="12" :cols="1">
<n-gi style="position: relative;top:1px;z-index: 19;width: 100%" v-if="telegraph.length>0">
<n-marquee :speed="120" >
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
{{item}}
</n-tag>
<!-- <n-text type="warning"> {{telegraph[0]}}</n-text>-->
</n-marquee>
</n-gi>
<n-gi style="padding-bottom: 70px;padding-top: 5px">
<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;"
v-model:value="activeKey"
mode="horizontal"
:options="menuOptions"
responsive
/>
</n-card>
</n-gi>
</n-grid>
</n-flex>
</n-watermark>
</n-dialog-provider>
</n-modal-provider>
</n-notification-provider>
</n-message-provider>
</n-config-provider>
</template>
<style>

View File

@@ -0,0 +1,216 @@
<script setup>
// import { MdPreview } from 'md-editor-v3';
// preview.css相比style.css少了编辑器那部分样式
import 'md-editor-v3/lib/preview.css';
import {h, onMounted, ref} from 'vue';
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
import {EventsOn} from "../../wailsjs/runtime";
import {NAvatar, NButton, useNotification} from "naive-ui";
const updateLog = ref('');
const versionInfo = ref('');
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
const alipay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/alipay.jpg')
const wxpay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/wxpay.jpg')
const notify = useNotification()
onMounted(() => {
document.title = '关于软件';
GetVersionInfo().then((res) => {
updateLog.value = res.content;
versionInfo.value = res.version;
icon.value = res.icon;
alipay.value=res.alipay;
wxpay.value=res.wxpay;
});
})
EventsOn("updateVersion",async (msg) => {
const githubTimeStr = msg.published_at;
// 创建一个 Date 对象
const utcDate = new Date(githubTimeStr);
// 获取本地时间
const date = new Date(utcDate.getTime());
const year = date.getFullYear();
// getMonth 返回值是 0 - 11所以要加 1
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
console.log("GitHub UTC 时间:", utcDate);
console.log("转换后的本地时间:", formattedDate);
notify.info({
avatar: () =>
h(NAvatar, {
size: 'small',
round: false,
src: icon.value
}),
title: '发现新版本: ' + msg.tag_name,
content: () => {
//return h(MdPreview, {theme:'dark',modelValue:msg.commit?.message}, null)
return h('div', {
style: {
'text-align': 'left',
'font-size': '14px',
}
}, { default: () => msg.commit?.message })
},
duration: 5000,
meta: "发布时间:"+formattedDate,
action: () => {
return h(NButton, {
type: 'primary',
size: 'small',
onClick: () => {
window.open(msg.html_url)
}
}, { default: () => '查看' })
}
})
})
</script>
<template>
<n-space vertical size="large">
<!-- 软件描述 -->
<n-card size="large">
<n-divider title-placement="center">关于软件</n-divider>
<n-space vertical >
<n-image width="100" :src="icon" />
<h1>
<n-badge :value="versionInfo" :offset="[50,10]" type="success">
<n-gradient-text type="info" :size="50" >go-stock</n-gradient-text>
</n-badge>
</h1>
<n-button size="tiny" @click="CheckUpdate" type="info" tertiary >检查更新</n-button>
<div style="justify-self: center;text-align: left" >
<p>自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
<p>支持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>
本软件仅供学习研究AI分析股票结果仅供参考不提供任何投资建议或决策
</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>
</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>
<p>
邮箱<a href="mailto:sparkmemory@163.com">sparkmemory@163.com</a><n-divider vertical />
QQ 506808970<n-divider vertical />
微信ArvinLovegood</p>
<p style="color: #FAA04A">*加微信或者QQ时请先备注或留言需求(<a href="#support">技术支持</a>功能建议商业咨询等否则会被忽略)</p>
<p>开源不易如果觉得好用可以请作者喝杯咖啡</p>
<n-flex justify="center">
<n-image width="200" :src="alipay" />
<n-image width="200" :src="wxpay" />
</n-flex>
</n-space>
<n-divider title-placement="center">鸣谢</n-divider>
<div style="justify-self: center;text-align: left" >
<p>
感谢以下捐赠者
<n-gradient-text size="small" type="warning">*</n-gradient-text><n-divider vertical />
</p>
<p>
感谢以下开发者
<a href="https://github.com/gnim2600" target="_blank">@gnim2600</a><n-divider vertical />
<a href="https://github.com/XXXiaohuayanGGG" target="_blank">@XXXiaohuayanGGG</a><n-divider vertical />
<a href="https://github.com/2lovecode" target="_blank">@2lovecode</a><n-divider vertical />
<a href="https://github.com/JerryLookupU" target="_blank">@JerryLookupU</a><n-divider vertical />
</p>
<p>
感谢以下开源项目
<a href="https://github.com/wailsapp/wails" target="_blank">Wails</a><n-divider vertical />
<a href="https://github.com/vuejs" target="_blank">Vue</a><n-divider vertical />
<a href="https://github.com/tusen-ai/naive-ui" target="_blank">NaiveUI</a><n-divider vertical />
</p>
</div>
<n-divider title-placement="center">关于版权和技术支持申明</n-divider>
<div style="justify-self: center;text-align: left" >
<p>
如需软件商业授权或定制开发请联系作者微信(备注 商业咨询)ArvinLovegood
</p>
<n-divider/>
<p>
本软件基于开源技术构建使用WailsNaiveUIVue等开源项目技术上如有问题可以先向对应的开源社区请求帮助
</p>
<p>
开源不易本人精力和时间有限如确实需要一对一技术支持请先赞助联系微信(备注 技术支持)ArvinLovegood
</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

@@ -0,0 +1,302 @@
<script setup>
import {onMounted, ref} from "vue";
import {ExportConfig, GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App";
import {useMessage} from "naive-ui";
import {data} from "../../wailsjs/go/models";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage()
const formRef = ref(null)
const formValue = ref({
ID:1,
tushareToken:'',
dingPush:{
enable:false,
dingRobot: ''
},
localPush:{
enable:true,
},
updateBasicInfoOnStart:false,
refreshInterval:1,
openAI:{
enable:false,
baseUrl: 'https://api.deepseek.com',
apiKey: '',
model: 'deepseek-chat',
temperature: 0.1,
maxTokens: 1024,
prompt:"",
timeout: 5,
questionTemplate: "{{stockName}}分析和总结",
crawlTimeOut:30,
kDays:30,
},
enableDanmu:false,
})
onMounted(()=>{
GetConfig().then(res=>{
formValue.value.ID = res.ID
formValue.value.tushareToken = res.tushareToken
formValue.value.dingPush = {
enable:res.dingPushEnable,
dingRobot:res.dingRobot
}
formValue.value.localPush = {
enable:res.localPushEnable,
}
formValue.value.updateBasicInfoOnStart = res.updateBasicInfoOnStart
formValue.value.refreshInterval = res.refreshInterval
formValue.value.openAI = {
enable:res.openAiEnable,
baseUrl: res.openAiBaseUrl,
apiKey:res.openAiApiKey,
model:res.openAiModelName,
temperature:res.openAiTemperature,
maxTokens:res.openAiMaxTokens,
prompt:res.prompt,
timeout:res.openAiApiTimeOut,
questionTemplate:res.questionTemplate?res.questionTemplate:'{{stockName}}分析和总结',
crawlTimeOut:res.crawlTimeOut,
kDays:res.kDays,
}
formValue.value.enableDanmu = res.enableDanmu
console.log(res)
})
//message.info("加载完成")
})
function saveConfig(){
let config= new data.Settings({
ID:formValue.value.ID,
dingPushEnable:formValue.value.dingPush.enable,
dingRobot:formValue.value.dingPush.dingRobot,
localPushEnable:formValue.value.localPush.enable,
updateBasicInfoOnStart:formValue.value.updateBasicInfoOnStart,
refreshInterval:formValue.value.refreshInterval,
openAiEnable:formValue.value.openAI.enable,
openAiBaseUrl:formValue.value.openAI.baseUrl,
openAiApiKey:formValue.value.openAI.apiKey,
openAiModelName:formValue.value.openAI.model,
openAiMaxTokens:formValue.value.openAI.maxTokens,
openAiTemperature:formValue.value.openAI.temperature,
tushareToken:formValue.value.tushareToken,
prompt:formValue.value.openAI.prompt,
openAiApiTimeOut:formValue.value.openAI.timeout,
questionTemplate:formValue.value.openAI.questionTemplate,
crawlTimeOut:formValue.value.openAI.crawlTimeOut,
kDays:formValue.value.openAI.kDays,
enableDanmu:formValue.value.enableDanmu
})
//console.log("Settings",config)
UpdateConfig(config).then(res=>{
message.success(res)
})
}
function getHeight() {
return document.documentElement.clientHeight
}
function sendTestNotice(){
let markdown="### go-stock test\n"+new Date()
let msg='{' +
' "msgtype": "markdown",' +
' "markdown": {' +
' "title":"go-stock'+new Date()+'",' +
' "text": "'+markdown+'"' +
' },' +
' "at": {' +
' "isAtAll": true' +
' }' +
' }'
SendDingDingMessageByType(msg, "test-"+new Date().getTime(),1).then(res=>{
message.info(res)
})
}
function exportConfig(){
ExportConfig().then(res=>{
message.info(res)
})
}
function importConfig(){
let input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
let file = e.target.files[0];
let reader = new FileReader();
reader.onload = (e) => {
let config = JSON.parse(e.target.result);
console.log(config)
formValue.value.ID = config.ID
formValue.value.tushareToken = config.tushareToken
formValue.value.dingPush = {
enable:config.dingPushEnable,
dingRobot:config.dingRobot
}
formValue.value.localPush = {
enable:config.localPushEnable,
}
formValue.value.updateBasicInfoOnStart = config.updateBasicInfoOnStart
formValue.value.refreshInterval = config.refreshInterval
formValue.value.openAI = {
enable:config.openAiEnable,
baseUrl: config.openAiBaseUrl,
apiKey:config.openAiApiKey,
model:config.openAiModelName,
temperature:config.openAiTemperature,
maxTokens:config.openAiMaxTokens,
prompt:config.prompt,
timeout:config.openAiApiTimeOut,
questionTemplate:config.questionTemplate,
crawlTimeOut:config.crawlTimeOut,
kDays:config.kDays
}
formValue.value.enableDanmu = config.enableDanmu
// formRef.value.resetFields()
};
reader.readAsText(file);
};
input.click();
}
window.onerror = function (event, source, lineno, colno, error) {
console.log(event, source, lineno, colno, error)
// 将错误信息发送给后端
EventsEmit("frontendError", {
page: "settings.vue",
message: event,
source: source,
lineno: lineno,
colno: colno,
error: error ? error.stack : null
});
//message.error("发生错误:"+event)
return true;
};
</script>
<template>
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px">
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'">
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24">
<n-text type="default" style="font-size: 25px;font-weight: bold">基础设置</n-text>
</n-gi>
<n-form-item-gi :span="10" label="Tushare api token" path="tushareToken" >
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
</n-form-item-gi>
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
</n-form-item-gi>
<n-form-item-gi :span="5" label="数据刷新间隔(重启生效)" path="refreshInterval" >
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
<template #suffix>
</template>
</n-input-number>
</n-form-item-gi>
</n-grid>
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24">
<n-text type="default" style="font-size: 25px;font-weight: bold">通知设置</n-text>
</n-gi>
<n-form-item-gi :span="6" label="是否启用钉钉推送:" path="dingPush.enable" >
<n-switch v-model:value="formValue.dingPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="6" label="是否启用本地推送:" path="localPush.enable" >
<n-switch v-model:value="formValue.localPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="5" label="弹幕功能:" path="enableDanmu" >
<n-switch v-model:value="formValue.enableDanmu" />
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
</n-form-item-gi>
</n-grid>
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
<n-gi :span="24">
<n-text type="default" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
</n-gi>
<n-form-item-gi :span="3" label="是否启用AI诊股" path="openAI.enable" >
<n-switch v-model:value="formValue.openAI.enable" />
</n-form-item-gi>
<n-form-item-gi :span="9" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl">
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="AI Timeout(秒)" title="AI请求超时时间(秒)" path="openAI.timeout">
<n-input-number min="60" step="1" placeholder="AI请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="Crawler Timeout(秒)" title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut">
<n-input-number min="30" step="1" placeholder="资讯采集超时时间(秒)" v-model:value="formValue.openAI.crawlTimeOut" />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey)" path="openAI.apiKey">
<n-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称" path="openAI.model">
<n-input type="text" placeholder="AI模型名称" v-model:value="formValue.openAI.model" clearable />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI temperature" path="openAI.temperature" >
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="openAI maxTokens" path="openAI.maxTokens">
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天)" path="openAI.maxTokens">
<n-input-number min="30" step="1" max="365" placeholder="日K线数据(天)" title="天数越多消耗tokens越多" v-model:value="formValue.openAI.kDays"/>
</n-form-item-gi>
<n-form-item-gi :span="22" 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="22" 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: 2,
maxRows: 5
}"
/>
</n-form-item-gi>
</n-grid>
<n-gi :span="24">
<n-space justify="center">
<n-button type="primary" @click="saveConfig">
保存
</n-button>
<n-button type="info" @click="exportConfig">
导出
</n-button>
<n-button type="error" @click="importConfig">
导入
</n-button>
</n-space>
</n-gi>
</n-form>
</n-flex>
</template>
<style scoped>
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
import {createApp} from 'vue'
import naive from 'naive-ui'
import App from './App.vue'
import router from './router/router'
const app = createApp(App)
app.use(router)
app.use(naive)
app.mount('#app')

View File

@@ -0,0 +1,18 @@
import { createMemoryHistory, createRouter } from 'vue-router'
import stockView from '../components/stock.vue'
import settingsView from '../components/settings.vue'
import about from "../components/about.vue";
const routes = [
{ path: '/', component: stockView,name: 'stock' },
{ path: '/settings/:id', component: settingsView,name: 'settings' },
{ path: '/about', component: about,name: 'about' },
]
const router = createRouter({
history: createMemoryHistory(),
routes,
})
export default router

View File

@@ -1,15 +1,40 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {models} from '../models';
import {data} from '../models';
export function CheckUpdate():Promise<void>;
export function ExportConfig():Promise<string>;
export function Follow(arg1:string):Promise<string>;
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
export function GetConfig():Promise<data.Settings>;
export function GetFollowList():Promise<Array<data.FollowedStock>>;
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
export function GetVersionInfo():Promise<models.VersionInfo>;
export function Greet(arg1:string):Promise<data.StockInfo>;
export function NewChatStream(arg1:string,arg2:string,arg3:string):Promise<void>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
export function SendDingDingMessageByType(arg1:string,arg2:string,arg3:number):Promise<string>;
export function SetAlarmChangePercent(arg1:number,arg2:number,arg3:string):Promise<string>;
export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>;
export function SetStockSort(arg1:number,arg2:string):Promise<void>;
export function UnFollow(arg1:string):Promise<string>;
export function UpdateConfig(arg1:data.Settings):Promise<string>;

View File

@@ -2,10 +2,26 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function CheckUpdate() {
return window['go']['main']['App']['CheckUpdate']();
}
export function ExportConfig() {
return window['go']['main']['App']['ExportConfig']();
}
export function Follow(arg1) {
return window['go']['main']['App']['Follow'](arg1);
}
export function GetAIResponseResult(arg1) {
return window['go']['main']['App']['GetAIResponseResult'](arg1);
}
export function GetConfig() {
return window['go']['main']['App']['GetConfig']();
}
export function GetFollowList() {
return window['go']['main']['App']['GetFollowList']();
}
@@ -14,14 +30,46 @@ export function GetStockList(arg1) {
return window['go']['main']['App']['GetStockList'](arg1);
}
export function GetVersionInfo() {
return window['go']['main']['App']['GetVersionInfo']();
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function NewChatStream(arg1, arg2, arg3) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3);
}
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
}
export function SendDingDingMessage(arg1, arg2) {
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
}
export function SendDingDingMessageByType(arg1, arg2, arg3) {
return window['go']['main']['App']['SendDingDingMessageByType'](arg1, arg2, arg3);
}
export function SetAlarmChangePercent(arg1, arg2, arg3) {
return window['go']['main']['App']['SetAlarmChangePercent'](arg1, arg2, arg3);
}
export function SetCostPriceAndVolume(arg1, arg2, arg3) {
return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3);
}
export function SetStockSort(arg1, arg2) {
return window['go']['main']['App']['SetStockSort'](arg1, arg2);
}
export function UnFollow(arg1) {
return window['go']['main']['App']['UnFollow'](arg1);
}
export function UpdateConfig(arg1) {
return window['go']['main']['App']['UpdateConfig'](arg1);
}

View File

@@ -8,9 +8,12 @@ export namespace data {
Price: number;
PriceChange: number;
ChangePercent: number;
AlarmChangePercent: number;
AlarmPrice: number;
// Go type: time
Time: any;
Sort: number;
IsDel: number;
static createFrom(source: any = {}) {
return new FollowedStock(source);
@@ -25,8 +28,88 @@ export namespace data {
this.Price = source["Price"];
this.PriceChange = source["PriceChange"];
this.ChangePercent = source["ChangePercent"];
this.AlarmChangePercent = source["AlarmChangePercent"];
this.AlarmPrice = source["AlarmPrice"];
this.Time = this.convertValues(source["Time"], null);
this.Sort = source["Sort"];
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 Settings {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
tushareToken: string;
localPushEnable: boolean;
dingPushEnable: boolean;
dingRobot: string;
updateBasicInfoOnStart: boolean;
refreshInterval: number;
openAiEnable: boolean;
openAiBaseUrl: string;
openAiApiKey: string;
openAiModelName: string;
openAiMaxTokens: number;
openAiTemperature: number;
openAiApiTimeOut: number;
prompt: string;
checkUpdate: boolean;
questionTemplate: string;
crawlTimeOut: number;
kDays: number;
enableDanmu: boolean;
static createFrom(source: any = {}) {
return new Settings(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.tushareToken = source["tushareToken"];
this.localPushEnable = source["localPushEnable"];
this.dingPushEnable = source["dingPushEnable"];
this.dingRobot = source["dingRobot"];
this.updateBasicInfoOnStart = source["updateBasicInfoOnStart"];
this.refreshInterval = source["refreshInterval"];
this.openAiEnable = source["openAiEnable"];
this.openAiBaseUrl = source["openAiBaseUrl"];
this.openAiApiKey = source["openAiApiKey"];
this.openAiModelName = source["openAiModelName"];
this.openAiMaxTokens = source["openAiMaxTokens"];
this.openAiTemperature = source["openAiTemperature"];
this.openAiApiTimeOut = source["openAiApiTimeOut"];
this.prompt = source["prompt"];
this.checkUpdate = source["checkUpdate"];
this.questionTemplate = source["questionTemplate"];
this.crawlTimeOut = source["crawlTimeOut"];
this.kDays = source["kDays"];
this.enableDanmu = source["enableDanmu"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@@ -132,13 +215,14 @@ export namespace data {
"时间": string;
"股票代码": string;
"股票名称": string;
"上次当前价格": number;
"当前价格": string;
"成交的股票数": string;
"成交金额": string;
"今日开盘价": string;
"昨日收盘价": string;
"今日最低价": string;
"今日最高价": string;
"今日最低价": string;
"竞买价": string;
"竞卖价": string;
"买一报价": string;
@@ -161,6 +245,18 @@ export namespace data {
"卖四申报": string;
"卖五报价": string;
"卖五申报": string;
changePercent: number;
changePrice: number;
highRate: number;
lowRate: number;
costPrice: number;
costVolume: number;
profit: number;
profitAmount: number;
profitAmountToday: number;
sort: number;
alarmChangePercent: number;
alarmPrice: number;
static createFrom(source: any = {}) {
return new StockInfo(source);
@@ -176,13 +272,14 @@ export namespace data {
this["时间"] = source["时间"];
this["股票代码"] = source["股票代码"];
this["股票名称"] = source["股票名称"];
this["上次当前价格"] = source["上次当前价格"];
this["当前价格"] = source["当前价格"];
this["成交的股票数"] = source["成交的股票数"];
this["成交金额"] = source["成交金额"];
this["今日开盘价"] = source["今日开盘价"];
this["昨日收盘价"] = source["昨日收盘价"];
this["今日最低价"] = source["今日最低价"];
this["今日最高价"] = source["今日最高价"];
this["今日最低价"] = source["今日最低价"];
this["竞买价"] = source["竞买价"];
this["竞卖价"] = source["竞卖价"];
this["买一报价"] = source["买一报价"];
@@ -205,6 +302,129 @@ export namespace data {
this["卖四申报"] = source["卖四申报"];
this["卖五报价"] = source["卖五报价"];
this["卖五申报"] = source["卖五申报"];
this.changePercent = source["changePercent"];
this.changePrice = source["changePrice"];
this.highRate = source["highRate"];
this.lowRate = source["lowRate"];
this.costPrice = source["costPrice"];
this.costVolume = source["costVolume"];
this.profit = source["profit"];
this.profitAmount = source["profitAmount"];
this.profitAmountToday = source["profitAmountToday"];
this.sort = source["sort"];
this.alarmChangePercent = source["alarmChangePercent"];
this.alarmPrice = source["alarmPrice"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}
export namespace models {
export class AIResponseResult {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
chatId: string;
modelName: string;
stockCode: string;
stockName: string;
question: string;
content: string;
IsDel: number;
static createFrom(source: any = {}) {
return new AIResponseResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.chatId = source["chatId"];
this.modelName = source["modelName"];
this.stockCode = source["stockCode"];
this.stockName = source["stockName"];
this.question = source["question"];
this.content = source["content"];
this.IsDel = source["IsDel"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class VersionInfo {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
version: string;
content: string;
icon: string;
alipay: string;
wxpay: string;
buildTimeStamp: number;
IsDel: number;
static createFrom(source: any = {}) {
return new VersionInfo(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.version = source["version"];
this.content = source["content"];
this.icon = source["icon"];
this.alipay = source["alipay"];
this.wxpay = source["wxpay"];
this.buildTimeStamp = source["buildTimeStamp"];
this.IsDel = source["IsDel"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {

View File

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

66
go.mod
View File

@@ -1,54 +1,82 @@
module go-stock
go 1.21
go 1.23
toolchain go1.23.0
require (
github.com/PuerkitoBio/goquery v1.10.1
github.com/chromedp/chromedp v0.11.2
github.com/coocood/freecache v1.2.4
github.com/duke-git/lancet/v2 v2.3.4
github.com/getlantern/systray v1.2.2
github.com/glebarez/sqlite v1.11.0
github.com/go-resty/resty/v2 v2.16.2
github.com/wailsapp/wails/v2 v2.9.2
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/stretchr/testify v1.10.0
github.com/wailsapp/wails/v2 v2.10.1
go.uber.org/zap v1.27.0
golang.org/x/text v0.16.0
golang.org/x/sys v0.30.0
golang.org/x/text v0.22.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3
gorm.io/plugin/soft_delete v1.2.1
)
require (
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/labstack/echo/v4 v4.10.2 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
github.com/leaanthony/gosod v1.0.3 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.0 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/tkrajina/go-reflector v0.5.6 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.16 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/net v0.35.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect

203
go.sum
View File

@@ -1,5 +1,19 @@
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb h1:noKVm2SsG4v0Yd0lHNtFYc9EUxIVvrr4kJ6hM8wvIYU=
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM=
github.com/chromedp/chromedp v0.11.2 h1:ZRHTh7DjbNTlfIv3NFTbB7eVeu5XCNkgrpcGSpn2oX0=
github.com/chromedp/chromedp v0.11.2/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -7,54 +21,100 @@ github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvY
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -63,27 +123,27 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/go-webview2 v1.0.16 h1:wffnvnkkLvhRex/aOrA3R7FP7rkvOqL/bir1br7BekU=
github.com/wailsapp/go-webview2 v1.0.16/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -93,71 +153,108 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU=
gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=

187
main.go
View File

@@ -3,16 +3,24 @@ package main
import (
"embed"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/models"
"log"
"os"
goruntime "runtime"
"runtime/debug"
"time"
)
//go:embed frontend/dist
@@ -21,50 +29,127 @@ var assets embed.FS
//go:embed build/appicon.png
var icon []byte
//go:embed build/app.ico
var icon2 []byte
//go:embed build/screenshot/alipay.jpg
var alipay []byte
//go:embed build/screenshot/wxpay.jpg
var wxpay []byte
//go:embed build/stock_basic.json
var stocksBin []byte
//go:embed build/stock_base_info_hk.json
var stocksBinHK []byte
//go:embed build/stock_base_info_us.json
var stocksBinUS []byte
//go:generate cp -R ./data ./build/bin
var Version string
var VersionCommit string
func main() {
checkDir("data")
db.Init("")
db.Dao.AutoMigrate(&data.StockInfo{})
db.Dao.AutoMigrate(&data.StockBasic{})
db.Dao.AutoMigrate(&data.FollowedStock{})
db.Dao.AutoMigrate(&data.IndexBasic{})
db.Dao.AutoMigrate(&data.Settings{})
db.Dao.AutoMigrate(&models.AIResponseResult{})
db.Dao.AutoMigrate(&models.StockInfoHK{})
db.Dao.AutoMigrate(&models.StockInfoUS{})
if stocksBin != nil && len(stocksBin) > 0 {
initStockData()
go initStockData()
}
if stocksBinHK != nil && len(stocksBinHK) > 0 {
go initStockDataHK()
}
if stocksBinUS != nil && len(stocksBinUS) > 0 {
go initStockDataUS()
}
data.NewStockDataApi().GetStockBaseInfo()
updateBasicInfo()
// Create an instance of the app structure
app := NewApp()
AppMenu := menu.NewMenu()
FileMenu := AppMenu.AddSubmenu("设置")
FileMenu.AddText("显示搜索框", keys.CmdOrCtrl("s"), func(callbackData *menu.CallbackData) {
runtime.EventsEmit(app.ctx, "showSearch", 1)
})
FileMenu.AddText("隐藏搜索框", keys.CmdOrCtrl("d"), func(callbackData *menu.CallbackData) {
runtime.EventsEmit(app.ctx, "showSearch", 0)
})
FileMenu.AddText("刷新数据", keys.CmdOrCtrl("r"), func(callbackData *menu.CallbackData) {
//runtime.EventsEmit(app.ctx, "refresh", "setting-"+time.Now().Format("2006-01-02 15:04:05"))
runtime.EventsEmit(app.ctx, "refreshFollowList", "refresh-"+time.Now().Format("2006-01-02 15:04:05"))
})
FileMenu.AddSeparator()
FileMenu.AddText("窗口全屏", keys.CmdOrCtrl("f"), func(callback *menu.CallbackData) {
runtime.WindowFullscreen(app.ctx)
})
FileMenu.AddText("窗口还原", keys.Key("Esc"), func(callback *menu.CallbackData) {
runtime.WindowUnfullscreen(app.ctx)
})
if goruntime.GOOS == "windows" {
FileMenu.AddText("隐藏到托盘区", keys.CmdOrCtrl("h"), func(_ *menu.CallbackData) {
runtime.WindowHide(app.ctx)
})
FileMenu.AddText("显示", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) {
runtime.WindowShow(app.ctx)
})
}
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
// runtime.Quit(app.ctx)
//})
logger.NewDefaultLogger().Info("version: " + Version)
logger.NewDefaultLogger().Info("commit: " + VersionCommit)
// Create application with options
var width, height int
var err error
width, height, err = getScreenResolution()
if err != nil {
logger.NewDefaultLogger().Error("get screen resolution error")
width = 1366
height = 768
}
// Create application with options
err := wails.Run(&options.App{
Title: "go-stock",
Width: 1366,
Height: 860,
MinWidth: 1024,
MinHeight: 768,
MaxWidth: 1280,
MaxHeight: 960,
DisableResize: false,
Fullscreen: false,
Frameless: false,
StartHidden: false,
HideWindowOnClose: false,
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
Assets: assets,
Menu: nil,
Logger: nil,
LogLevel: logger.DEBUG,
OnStartup: app.startup,
OnDomReady: app.domReady,
OnBeforeClose: app.beforeClose,
OnShutdown: app.shutdown,
WindowStartState: options.Normal,
err = wails.Run(&options.App{
Title: "go-stock",
Width: width * 4 / 5,
Height: height * 4 / 5,
MinWidth: 1024,
MinHeight: 768,
MaxWidth: width,
MaxHeight: height,
DisableResize: false,
Fullscreen: false,
Frameless: true,
StartHidden: false,
HideWindowOnClose: false,
EnableDefaultContextMenu: true,
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
Assets: assets,
Menu: AppMenu,
Logger: nil,
LogLevel: logger.DEBUG,
LogLevelProduction: logger.ERROR,
OnStartup: app.startup,
OnDomReady: app.domReady,
OnBeforeClose: app.beforeClose,
OnShutdown: app.shutdown,
WindowStartState: options.Normal,
Bind: []interface{}{
app,
},
@@ -100,6 +185,50 @@ func main() {
if err != nil {
log.Fatal(err)
}
}
func initStockDataUS() {
var count int64
db.Dao.Model(&models.StockInfoUS{}).Count(&count)
if count > 0 {
return
}
var v []models.StockInfoUS
err := json.Unmarshal(stocksBinUS, &v)
if err != nil {
return
}
for _, item := range v {
db.Dao.Model(&models.StockInfoUS{}).Create(&item)
}
log.Printf("init stock data us %d", len(v))
}
func initStockDataHK() {
var count int64
db.Dao.Model(&models.StockInfoHK{}).Count(&count)
if count > 0 {
return
}
var v []models.StockInfoHK
err := json.Unmarshal(stocksBinHK, &v)
if err != nil {
return
}
for _, item := range v {
db.Dao.Model(&models.StockInfoHK{}).Create(&item)
}
log.Printf("init stock data hk %d", len(v))
}
func updateBasicInfo() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
if config.UpdateBasicInfoOnStart {
//更新基本信息
go data.NewStockDataApi().GetStockBaseInfo()
go data.NewStockDataApi().GetIndexBasic()
}
}
func initStockData() {
@@ -145,3 +274,11 @@ func checkDir(dir string) {
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

@@ -1 +0,0 @@
{"request_id":"359a096c-0a5a-4b07-818f-3241738d5e73","code":40203,"data":null,"msg":"抱歉您每小时最多访问该接口1次权限的具体详情访问https://tushare.pro/document/1?doc_id=108。"}

View File

@@ -7,13 +7,13 @@
"frontend:dev:serverUrl": "http://localhost:5173",
"author": {
"name": "spark",
"email": "wzl@huazx.cn"
"email": "sparkmemory@163.com"
},
"info": {
"companyName": "sparkmemory",
"productName": "go-stock",
"productVersion": "1.0.0",
"copyright": "Copyright#sparkmemory@163.com",
"comments": "股票行情实时获取"
"comments": "股票行情实时获取,AI赋能分析股票,支持DeepSeekOpenAI OllamaLMStudioAnythingLLM硅基流动火山方舟阿里云百炼等平台或模型。软件发行版见GitHubhttps://github.com/ArvinLovegood/go-stock"
}
}