mirror of
https://github.com/ArvinLovegood/go-stock.git
synced 2025-07-19 00:00:09 +08:00
feat(app): 添加钉钉消息发送功能和股票涨跌报警
- 新增 SendDingDingMessage 和 SetAlarmChangePercent 函数- 实现钉钉消息发送和股票涨跌报警逻辑 - 更新前端界面,增加报警值设置和消息发送功能 - 新增 DingDingAPI 结构体和相关方法
This commit is contained in:
parent
685a7d23b2
commit
2f2b19f5d7
28
app.go
28
app.go
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/logger"
|
||||
@ -9,12 +10,17 @@ import (
|
||||
|
||||
// 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
|
||||
@ -90,3 +96,21 @@ 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 float64, stockCode string) string {
|
||||
return data.NewStockDataApi().SetAlarmChangePercent(val, 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)
|
||||
}
|
||||
|
77
backend/data/dingding_api.go
Normal file
77
backend/data/dingding_api.go
Normal file
@ -0,0 +1,77 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/logger"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/1/3 13:53
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
const dingding_robot_url = "https://oapi.dingtalk.com/robot/send?access_token=0237527b404598f37ae5d83ef36e936860c7ba5d3892cd43f64c4159d3ed7cb1"
|
||||
|
||||
type DingDingAPI struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewDingDingAPI() *DingDingAPI {
|
||||
return &DingDingAPI{
|
||||
client: resty.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
// 发送钉钉消息
|
||||
resp, err := resty.New().R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(message).
|
||||
Post(dingding_robot_url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "发送钉钉消息失败"
|
||||
}
|
||||
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
|
||||
return "发送钉钉消息成功"
|
||||
}
|
||||
|
||||
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(dingding_robot_url)
|
||||
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"`
|
||||
}
|
31
backend/data/dingding_api_test.go
Normal file
31
backend/data/dingding_api_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/1/3 13:53
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestRobot(t *testing.T) {
|
||||
resp, err := resty.New().R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(`{
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title":"go-stock",
|
||||
"text": "#### 杭州天气 @150XXXXXXXX \n > 9度,西北风1级,空气良89,相对温度73%\n > \n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n"
|
||||
},
|
||||
"at": {
|
||||
"isAtAll": true
|
||||
}
|
||||
}`).
|
||||
Post(dingding_robot_url)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Log(resp.String())
|
||||
}
|
@ -127,16 +127,17 @@ 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
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
StockCode string
|
||||
Name string
|
||||
Volume int64
|
||||
CostPrice float64
|
||||
Price float64
|
||||
PriceChange float64
|
||||
ChangePercent float64
|
||||
AlarmChangePercent float64
|
||||
Time time.Time
|
||||
Sort int64
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver FollowedStock) TableName() string {
|
||||
@ -309,6 +310,15 @@ func (receiver StockDataApi) SetCostPriceAndVolume(price float64, volume int64,
|
||||
return "设置成功"
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) SetAlarmChangePercent(val float64, stockCode string) string {
|
||||
err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", stockCode).Update("alarm_change_percent", val).Error
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return "设置失败"
|
||||
}
|
||||
return "设置成功"
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetFollowList() []FollowedStock {
|
||||
var result []FollowedStock
|
||||
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result)
|
||||
|
@ -1,6 +1,14 @@
|
||||
<script setup>
|
||||
import {onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
|
||||
import {Greet, Follow, UnFollow, GetFollowList, GetStockList, SetCostPriceAndVolume} from '../../wailsjs/go/main/App'
|
||||
import {
|
||||
Greet,
|
||||
Follow,
|
||||
UnFollow,
|
||||
GetFollowList,
|
||||
GetStockList,
|
||||
SetCostPriceAndVolume,
|
||||
SendDingDingMessage, SetAlarmChangePercent
|
||||
} from '../../wailsjs/go/main/App'
|
||||
import {NButton, NFlex, NForm, NFormItem, NInputNumber, NText, useMessage, useModal} from 'naive-ui'
|
||||
import { WindowFullscreen,WindowUnfullscreen,EventsOn } from '../../wailsjs/runtime'
|
||||
import {Add, StarOutline} from '@vicons/ionicons5'
|
||||
@ -22,7 +30,8 @@ const formModel = ref({
|
||||
name: "",
|
||||
code: "",
|
||||
costPrice: 0.000,
|
||||
volume: 0
|
||||
volume: 0,
|
||||
alarm: 0,
|
||||
})
|
||||
|
||||
const data = reactive({
|
||||
@ -171,7 +180,6 @@ async function monitor() {
|
||||
result.highRate=((result["今日最高价"]-result["今日开盘价"])*100/result["今日开盘价"]).toFixed(2)+"%"
|
||||
result.lowRate=((result["今日最低价"]-result["今日开盘价"])*100/result["今日开盘价"]).toFixed(2)+"%"
|
||||
|
||||
|
||||
if (roundedNum>0) {
|
||||
result.type="error"
|
||||
result.color="#E88080"
|
||||
@ -195,6 +203,9 @@ async function monitor() {
|
||||
}else if(result.profitAmount<0){
|
||||
result.profitType="success"
|
||||
}
|
||||
if(Math.abs(res[0].AlarmChangePercent)>0&&roundedNum>res[0].AlarmChangePercent){
|
||||
SendMessage(result)
|
||||
}
|
||||
}
|
||||
results.value[result["股票名称"]]=result
|
||||
})
|
||||
@ -224,6 +235,7 @@ function setStock(code,name){
|
||||
formModel.value.code=code
|
||||
formModel.value.volume=res[0].Volume
|
||||
formModel.value.costPrice=res[0].CostPrice
|
||||
formModel.value.alarm=res[0].AlarmChangePercent
|
||||
modalShow.value=true
|
||||
}
|
||||
|
||||
@ -241,8 +253,13 @@ function showK(code,name){
|
||||
}
|
||||
|
||||
|
||||
function updateCostPriceAndVolumeNew(code,price,volume){
|
||||
function updateCostPriceAndVolumeNew(code,price,volume,alarm){
|
||||
console.log(code,price,volume)
|
||||
if(alarm){
|
||||
SetAlarmChangePercent(alarm,code).then(result => {
|
||||
//message.success(result)
|
||||
})
|
||||
}
|
||||
SetCostPriceAndVolume(code,price,volume).then(result => {
|
||||
modalShow.value=false
|
||||
message.success(result)
|
||||
@ -267,6 +284,33 @@ function fullscreen(){
|
||||
}
|
||||
data.fullscreen=!data.fullscreen
|
||||
}
|
||||
|
||||
function SendMessage(result){
|
||||
let img='http://image.sinajs.cn/newchart/min/n/'+result["股票代码"]+'.gif'+"?t="+Date.now()
|
||||
let markdown="### go-stock市场行情\n\n"+
|
||||
"### "+result["股票名称"]+"("+result["股票代码"]+")\n" +
|
||||
"- 当前价格: "+result["当前价格"]+" "+result.s+"\n" +
|
||||
"- 最高价: "+result["今日最高价"]+" "+result.highRate+"\n" +
|
||||
"- 最低价: "+result["今日最低价"]+" "+result.lowRate+"\n" +
|
||||
"- 昨收价: "+result["昨日收盘价"]+"\n" +
|
||||
"- 今开价: "+result["今日开盘价"]+"\n" +
|
||||
"- 成本价: "+result.costPrice+" "+result.profit+"% "+result.profitAmount+" ¥\n" +
|
||||
"- 成本数量: "+result.volume+"股\n" +
|
||||
"- 日期: "+result["日期"]+" "+result["时间"]+"\n\n"+
|
||||
"\n"
|
||||
let msg='{' +
|
||||
' "msgtype": "markdown",' +
|
||||
' "markdown": {' +
|
||||
' "title":"'+result["股票名称"]+"("+result["股票代码"]+") "+result["当前价格"]+" "+result.s+'",' +
|
||||
' "text": "'+markdown+'"' +
|
||||
' },' +
|
||||
' "at": {' +
|
||||
' "isAtAll": true' +
|
||||
' }' +
|
||||
' }'
|
||||
SendDingDingMessage(msg,result["股票代码"])
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -309,7 +353,7 @@ function fullscreen(){
|
||||
<n-button size="tiny" type="success" @click="showFenshi(result['股票代码'],result['股票名称'])"> 分时 </n-button>
|
||||
<n-button size="tiny" type="error" @click="showK(result['股票代码'],result['股票名称'])"> 日K </n-button>
|
||||
<n-button size="tiny" type="warning" @click="search(result['股票代码'],result['股票名称'])"> 详情 </n-button>
|
||||
|
||||
<!-- <n-button size="tiny" type="info" @click="SendMessage(result)"> 钉钉 </n-button>-->
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-card >
|
||||
@ -342,9 +386,12 @@ function fullscreen(){
|
||||
<n-form-item label="数量(股)" path="volume">
|
||||
<n-input-number v-model:value="formModel.volume" min="0" placeholder="请输入股票数量" />
|
||||
</n-form-item>
|
||||
<n-form-item label="涨跌报警值(%)" path="alarm">
|
||||
<n-input-number v-model:value="formModel.alarm" min="0" placeholder="请输入涨跌报警值(%)" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-button type="primary" @click="updateCostPriceAndVolumeNew(formModel.code,formModel.costPrice,formModel.volume)">保存</n-button>
|
||||
<n-button type="primary" @click="updateCostPriceAndVolumeNew(formModel.code,formModel.costPrice,formModel.volume,formModel.alarm)">保存</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
|
4
frontend/wailsjs/go/main/App.d.ts
vendored
4
frontend/wailsjs/go/main/App.d.ts
vendored
@ -10,6 +10,10 @@ export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
|
||||
|
||||
export function Greet(arg1:string):Promise<data.StockInfo>;
|
||||
|
||||
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function SetAlarmChangePercent(arg1:number,arg2:string):Promise<string>;
|
||||
|
||||
export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>;
|
||||
|
||||
export function UnFollow(arg1:string):Promise<string>;
|
||||
|
@ -18,6 +18,14 @@ export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function SendDingDingMessage(arg1, arg2) {
|
||||
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SetAlarmChangePercent(arg1, arg2) {
|
||||
return window['go']['main']['App']['SetAlarmChangePercent'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SetCostPriceAndVolume(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export namespace data {
|
||||
Price: number;
|
||||
PriceChange: number;
|
||||
ChangePercent: number;
|
||||
AlarmChangePercent: number;
|
||||
// Go type: time
|
||||
Time: any;
|
||||
Sort: number;
|
||||
@ -26,6 +27,7 @@ export namespace data {
|
||||
this.Price = source["Price"];
|
||||
this.PriceChange = source["PriceChange"];
|
||||
this.ChangePercent = source["ChangePercent"];
|
||||
this.AlarmChangePercent = source["AlarmChangePercent"];
|
||||
this.Time = this.convertValues(source["Time"], null);
|
||||
this.Sort = source["Sort"];
|
||||
this.IsDel = source["IsDel"];
|
||||
|
2
go.mod
2
go.mod
@ -5,6 +5,7 @@ go 1.21
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/coocood/freecache v1.2.4
|
||||
github.com/duke-git/lancet/v2 v2.3.4
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
@ -19,6 +20,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -1,5 +1,9 @@
|
||||
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/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=
|
||||
|
Loading…
x
Reference in New Issue
Block a user