diff --git a/Makefile b/Makefile index a20eefa..6be43c3 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ plats = linux darwin arch ?= amd64 archs = amd64 arm arm64 -all: stock ss +all: stock define build_app @echo 'building $(1) ...' @@ -17,10 +17,6 @@ stock: $(call build_app,stock,$(plat),$(arch)) .PHONY: stock -ss: - $(call build_app,ss,$(plat),$(arch)) -.PHONY: ss - clean: -@rm -f builder/* diff --git a/cmd/ss/main.go b/cmd/ss/main.go deleted file mode 100644 index e131668..0000000 --- a/cmd/ss/main.go +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @Author: jager - * @Email: lhj168os@gmail.com - * @File: main - * @Date: 2021/12/20 2:24 下午 - * @package: ss - * @Version: v1.0.0 - * - * @Description: - * - */ - -package main - -import ( - "github.com/gin-gonic/gin" - "github.com/jageros/hawox/contextx" - "github.com/jageros/hawox/httpx" - "github.com/jageros/hawox/logx" - "net/http" - "stock/msg" - "time" -) - -func main() { - ctx, cancel := contextx.Default() - defer cancel() - - logx.Init(logx.DebugLevel, logx.SetCaller(), logx.SetRequest()) - defer logx.Sync() - - httpx.InitializeHttpServer(ctx, func(engine *gin.Engine) { - r := engine.Group("/api") - r.GET("/sayhello", func(c *gin.Context) { - httpx.PkgMsgWrite(c, map[string]interface{}{"say": "hello world!"}) - }) - r.POST("/receive", func(c *gin.Context) { - rMsg := &msg.RData{} - err := c.BindXML(rMsg) - if err != nil { - logx.Error(err) - } else { - logx.Info(rMsg) - } - - wMsg := &xml{ - ToUserName: rMsg.FromUserName, - FromUserName: rMsg.ToUserName, - CreateTime: int(time.Now().Unix()), - MsgType: "text", - Content: "收到,谢谢!", - } - c.XML(http.StatusOK, wMsg) - - //c.XML(http.StatusOK, wMsg) - - //err = msg.Rcv(rMsg.FromUserName) - //if err != nil { - // logx.Error(err) - //} - - //c.String(http.StatusOK, "success") - }) - - r.GET("/receive", func(c *gin.Context) { - echostr := c.Query("echostr") - logx.Infof("=== %s ===", echostr) - c.String(http.StatusOK, echostr) - }) - }, func(s *httpx.Server) { - s.Mode = "debug" - s.Port = 8567 - }) - err := ctx.Wait() - logx.Infof("Stop With: %v", err) -} - -type xml struct { - ToUserName string `xml:"ToUserName"` - FromUserName string `xml:"FromUserName"` - CreateTime int `xml:"CreateTime"` - MsgType string `xml:"MsgType"` - Content string `xml:"Content"` -} diff --git a/cmd/stock/main.go b/cmd/stock/main.go index 4cb57fb..4863500 100644 --- a/cmd/stock/main.go +++ b/cmd/stock/main.go @@ -46,8 +46,14 @@ func main() { logx.Fatal(err) } + timer.AddTicker(time.Second*1200, func() { + logx.Info("开始保存用户数据进MongoDB...") + user.SaveAll() + logx.Info("保存用户数进MongoDB结束!") + + }) + stockCodes := user.Codes(false) - fundCodes := user.Codes(true) timer.RunEveryDay(9, 25, 0, func() { if !stock.IsSellDay(time.Now()) { @@ -80,12 +86,17 @@ func main() { logx.Info("今天不是交易日!") return } - text := fund.FundsMsg(fundCodes...) - if text == "" { - logx.Errorf("收集基金数据为空!") - return - } - err := msg.Send(text) + + fund.Clear() + user.ForEachUser(func(u module.IUser) bool { + codes := u.Codes(true) + stk := fund.NewFundArg(codes...) + err = wxgzh.Send(u.OpenID(), stk) + if err != nil { + logx.Error(err) + } + return true + }) if err != nil { logx.Errorf("dingtalk.SendMsg(基金信息) err: %v", err) } else { @@ -93,9 +104,11 @@ func main() { } }) - err = stock.Init(stockCodes...) - if err != nil { - logx.Fatal(err) + if len(stockCodes) > 0 { + err = stock.Init(stockCodes...) + if err != nil { + logx.Error(err) + } } ctx.Go(func(ctx contextx.Context) error { @@ -152,20 +165,6 @@ func main() { } return true }) - - //text := ss.Msg() - //if text == "" { - // logx.Info("已更新数据,未超过阈值,无警告!") - // continue - //} - //err = msg.Send(text) - //if err != nil { - // count++ - // logx.Errorf("SendMsg ErrCount=%d err=%v", count, err) - // if count > 10 { - // return err - // } - //} } } }) @@ -185,7 +184,13 @@ func main() { }) }, func(s *httpx.Server) { s.Mode = "debug" - s.Port = 8567 + s.Port = 16888 + }) + + ctx.Go(func(ctx contextx.Context) error { + <-ctx.Done() + user.SaveAll() + return nil }) err = ctx.Wait() // 等待停止信号 diff --git a/fund/fund.go b/fund/fund.go index 0ef6dce..cbb6a14 100644 --- a/fund/fund.go +++ b/fund/fund.go @@ -20,6 +20,7 @@ import ( "github.com/jageros/hawox/logx" "regexp" "strconv" + "sync" ) const ( @@ -28,17 +29,45 @@ const ( var ( jsonStr = regexp.MustCompile(`{(.*?)}`) + + fds = &funds{fdsMap: map[string]*fund{}} ) type fund struct { Code string `json:"fundcode"` - Name string `json:"name"` + FName string `json:"name"` UnitVal string `json:"dwjz"` EstimateVal string `json:"gsz"` RisePer string `json:"gszzl"` UpdateTime string `json:"gztime"` } +func (f *fund) Name() string { + return f.FName +} + +func (f *fund) Update() error { + url := fmt.Sprintf(baseUrl, f.Code) + result, err := httpc.Request(httpc.GET, url, httpc.FORM, nil, nil) + if err != nil { + return err + } + ss := jsonStr.FindStringSubmatch(string(result)) + if len(ss) <= 0 { + return errcode.New(404, "找不到基金:"+f.Code) + } + ff := &fund{} + err = json.Unmarshal([]byte(ss[0]), ff) + if err == nil { + f.FName = ff.FName + f.UnitVal = ff.UnitVal + f.EstimateVal = ff.EstimateVal + f.RisePer = ff.RisePer + f.UpdateTime = ff.UpdateTime + } + return err +} + func (f *fund) Msg() string { var rise string last, err1 := strconv.ParseFloat(f.UnitVal, 64) @@ -46,17 +75,35 @@ func (f *fund) Msg() string { if err1 == nil && err2 == nil { rise = fmt.Sprintf("%.5f", cur-last) } - msg := fmt.Sprintf(msgTemplate, f.Name, f.UpdateTime, f.UnitVal, f.EstimateVal, rise, f.RisePer) + msg := fmt.Sprintf(msgTemplate, f.FName, f.UpdateTime, f.UnitVal, f.EstimateVal, rise, f.RisePer) return msg } type funds struct { - codes []string + fdsMap map[string]*fund + mx sync.RWMutex } -func NewFunds(codes ...string) *funds { - return &funds{ - codes: codes, +func (fs *funds) getFund(code string) *fund { + fs.mx.RLock() + defer fs.mx.RUnlock() + if fd, ok := fs.fdsMap[code]; ok { + return fd + } + return nil +} + +func (fs *funds) addFund(fd *fund) { + fs.mx.Lock() + defer fs.mx.Unlock() + fs.fdsMap[fd.Code] = fd +} + +func Clear() { + fds.mx.Lock() + defer fds.mx.Unlock() + fds = &funds{ + fdsMap: map[string]*fund{}, } } @@ -67,17 +114,23 @@ func FundsMsg(codes ...string) string { //var msg = "基金定投估值 >>>\n" msg := "" for _, code := range codes { - fd, err := newFund(code) - if err != nil { - logx.Error(err) - continue + fd := fds.getFund(code) + if fd == nil { + fd_, err := NewFund(code) + if err == nil { + fd = fd_ + fds.addFund(fd_) + } else { + logx.Error(err) + continue + } } msg += fd.Msg() } return msg } -func newFund(code string) (*fund, error) { +func NewFund(code string) (*fund, error) { url := fmt.Sprintf(baseUrl, code) result, err := httpc.Request(httpc.GET, url, httpc.FORM, nil, nil) if err != nil { @@ -99,13 +152,27 @@ const msgTemplate = `%s ` -func (fs *funds) Arg(openid string) map[string]interface{} { +type fundArg struct { + codes []string +} + +func NewFundArg(codes ...string) *fundArg { + return &fundArg{ + codes: codes, + } +} + +func (fa *fundArg) Arg(openid string) map[string]interface{} { + msg := FundsMsg(fa.codes...) + if msg == "" { + return nil + } arg := map[string]interface{}{ "touser": openid, "template_id": "SQXWp3RiYySb2GYG-vMYDsSIm-KZNM9szVpFOryUQGQ", "data": map[string]interface{}{ "keyword": map[string]interface{}{ - "value": FundsMsg(fs.codes...), + "value": msg, "color": "#173177", }, }, diff --git a/stock/stock.go b/stock/stock.go index fb7d173..f795962 100644 --- a/stock/stock.go +++ b/stock/stock.go @@ -14,7 +14,7 @@ import ( var ( baseUrl = "https://hq.sinajs.cn/" - fds *stocks + fds = &stocks{stkMap: map[string]*stock{}} mx sync.RWMutex ) @@ -25,10 +25,11 @@ type IStock interface { } type stock struct { - code string - values []string - lastRise float64 - nowRise float64 + code string + values []string + lastRise float64 + nowRise float64 + nowRiseStr string } func (s *stock) Code() string { @@ -55,6 +56,7 @@ func (s *stock) notify() bool { rs := (nowPrice - lastPrice) / lastPrice * 100 s.nowRise = rs + s.nowRiseStr = fmt.Sprintf("%.2f%%", s.nowRise) if (s.lastRise == 0 && rs != 0) || rs-s.lastRise >= cfg.ThresholdValue || s.lastRise-rs >= cfg.ThresholdValue { s.lastRise = rs return true @@ -63,7 +65,21 @@ func (s *stock) notify() bool { } func (s *stock) rise() string { - return fmt.Sprintf("%.2f%%", s.nowRise) + if s.nowRiseStr != "" { + return s.nowRiseStr + } + lastPrice, err := strconv.ParseFloat(s.values[2], 64) + if err != nil || lastPrice == 0 { + return "" + } + nowPrice, err := strconv.ParseFloat(s.values[3], 64) + if err != nil { + return "" + } + + rs := (nowPrice - lastPrice) / lastPrice * 100 + s.nowRiseStr = fmt.Sprintf("%.2f%%", rs) + return s.nowRiseStr } func (s *stock) buyCount() string { @@ -144,18 +160,18 @@ func (s *stock) Msg() string { // ========================== func Init(codes ...string) error { - fds = &stocks{} + fds = &stocks{stkMap: map[string]*stock{}} return fds.AddCodes(codes...) } type stocks struct { - //codes []string - //ss []*stock stkMap map[string]*stock mx sync.RWMutex } func (sk *stocks) getStocks(codes ...string) *stocks { + mx.RLock() + defer mx.RUnlock() var stks = &stocks{} for _, code := range codes { stks.stkMap[code] = sk.stkMap[code] @@ -181,6 +197,9 @@ func (sk *stocks) AddCodes(codes ...string) error { } cds = append(cds, code) } + if len(cds) <= 0 { + return nil + } stks, err := newStocks(cds...) if err != nil { return err @@ -227,6 +246,17 @@ func (sk *stocks) Update() error { } func (sk *stocks) Msg() string { + sk.mx.RLock() + defer sk.mx.RUnlock() + var resp string + for _, s := range sk.stkMap { + msg := s.Msg() + resp = resp + msg + "\n" + } + return resp +} + +func (sk *stocks) msg() string { sk.mx.RLock() defer sk.mx.RUnlock() var resp string @@ -240,12 +270,16 @@ func (sk *stocks) Msg() string { } func (sk *stocks) Arg(openid string) map[string]interface{} { + msg := sk.msg() + if msg == "" { + return nil + } arg := map[string]interface{}{ "touser": openid, "template_id": "yWuLbhAy7TTuqdeB9-VS6CR_t2rZQ8MHkJ62MF3VlS8", "data": map[string]interface{}{ "keyword": map[string]interface{}{ - "value": sk.Msg(), + "value": msg, "color": "#173177", }, }, @@ -253,15 +287,28 @@ func (sk *stocks) Arg(openid string) map[string]interface{} { return arg } +func GetStock(code string) (*stock, error) { + if fds == nil { + fds = &stocks{stkMap: map[string]*stock{}} + } + err := fds.AddCodes(code) + if err != nil { + return nil, err + } + mx.RLock() + defer mx.RUnlock() + if fd, ok := fds.stkMap[code]; ok { + return fd, nil + } + return nil, errcode.New(1, "没有此股票数据") +} + func GetStocks(codes ...string) (*stocks, error) { if len(codes) <= 0 { return nil, errcode.New(1, "股票代码为空") } - - mx.Lock() - defer mx.Unlock() if fds == nil { - fds = &stocks{} + fds = &stocks{stkMap: map[string]*stock{}} } err := fds.AddCodes(codes...) if err != nil { @@ -291,8 +338,13 @@ func newStocks(codes ...string) ([]*stock, error) { for _, s := range strs { ss := strings.Split(s, "\"") if len(ss) >= 2 { + vs := strings.Split(ss[1], ",") + if len(vs) < 32 { + continue + } v := &stock{ - values: strings.Split(ss[1], ","), + code: ss[0][13:19], + values: vs, } stks = append(stks, v) } diff --git a/user/cache.go b/user/cache.go index 83ae573..9b1a82b 100644 --- a/user/cache.go +++ b/user/cache.go @@ -14,6 +14,7 @@ package user import ( "github.com/jageros/hawox/attribute" + "github.com/jageros/hawox/logx" "stock/module" "sync" ) @@ -37,6 +38,17 @@ func LoadAllUserIntoCache() error { return nil } +func SaveAll() { + mx.Lock() + defer mx.Unlock() + for _, u := range users { + err := u.attr.Save(true) + if err != nil { + logx.Error(err) + } + } +} + func GetUser(openId string) (*User, error) { mx.RLock() u, ok := users[openId] diff --git a/wxgzh/wxgzh.go b/wxgzh/wxgzh.go index 5b0fe10..396065e 100644 --- a/wxgzh/wxgzh.go +++ b/wxgzh/wxgzh.go @@ -19,7 +19,9 @@ import ( "github.com/jageros/hawox/httpc" "github.com/jageros/hawox/logx" "net/http" + "stock/fund" "stock/module" + "stock/stock" "stock/user" "strings" "time" @@ -52,6 +54,11 @@ type IArg interface { Arg(openid string) map[string]interface{} } +type IMsg interface { + Msg() string + Name() string +} + func getAccessToken(update bool) (string, error) { if !update && time.Now().Unix() < expiresIn { return accessToken, nil @@ -77,6 +84,9 @@ func send(openID string, arg IArg, recall bool) error { url := fmt.Sprintf(sendUrl, token) msg := arg.Arg(openID) + if msg == nil { + return errcode.New(1, "arg == nil") + } resp := &Resp{} err = httpc.RequestWithInterface(httpc.POST, url, httpc.JSON, msg, nil, resp) if err != nil { @@ -146,7 +156,8 @@ func Handle(c *gin.Context) { FromUserName: rMsg.ToUserName, MsgType: "text", CreateTime: time.Now().Unix(), - Content: "订阅股票:+st股票代码(例如:+st600905)\n订阅基金:+fd基金代码(例如:+fd161725)\n" + + Content: "查询股票:=st股票代码(例如:=st600905)\n查询基金:=fd基金代码(例如:=fd161725)\n\n" + + "订阅股票:+st股票代码(例如:+st600905)\n订阅基金:+fd基金代码(例如:+fd161725)\n\n" + "取消订阅股票:-st股票代码(例如:-st600905)\n取消订阅基金:-fd基金代码(例如:-fd161725)", } @@ -159,12 +170,55 @@ func Handle(c *gin.Context) { } switch { + case len(rMsg.Content) < 9: + break + + case strings.HasPrefix(rMsg.Content, "="): + code := rMsg.Content[3:] + isFund := rMsg.Content[1:3] == "fd" + var im IMsg + if isFund { + im, err = fund.NewFund(code) + } else { + im, err = stock.GetStock(code) + } + if err != nil { + wMsg.Content = "查询出错:\n" + err.Error() + } else { + wMsg.Content = "查询成功:\n" + im.Msg() + } + case strings.HasPrefix(rMsg.Content, "+"): - u.Subscribe(rMsg.Content[1:3] == "fd", rMsg.Content[3:]) - wMsg.Content = "订阅成功!" + code := rMsg.Content[3:] + isFund := rMsg.Content[1:3] == "fd" + var im IMsg + if isFund { + im, err = fund.NewFund(code) + } else { + im, err = stock.GetStock(code) + } + if err != nil { + wMsg.Content = "订阅出错:\n" + err.Error() + } else { + u.Subscribe(isFund, code) + wMsg.Content = "订阅成功:\n" + im.Msg() + } + case strings.HasPrefix(rMsg.Content, "-"): - u.UnSubscribe(rMsg.Content[1:3] == "fd", rMsg.Content[3:]) - wMsg.Content = "成功取消订阅!" + code := rMsg.Content[3:] + isFund := rMsg.Content[1:3] == "fd" + var im IMsg + if isFund { + im, err = fund.NewFund(code) + } else { + im, err = stock.GetStock(code) + } + if err != nil { + wMsg.Content = "取消订阅:\n" + err.Error() + } else { + wMsg.Content = "成功取消订阅:" + im.Name() + } + u.UnSubscribe(isFund, code) } }