diff --git a/handler/newbond/model.go b/handler/newbond/model.go new file mode 100644 index 0000000..579e463 --- /dev/null +++ b/handler/newbond/model.go @@ -0,0 +1,138 @@ +package newbond + +import ( + "database/sql/driver" + "fmt" + "time" +) + +type BondData struct { + SecurityCode string `json:"SECURITY_CODE" db:"SecurityCode"` + SecuCode string `json:"SECUCODE" db:"SecuCode"` + TradeMarket string `json:"TRADE_MARKET" db:"TradeMarket"` + SecurityNameAbbr string `json:"SECURITY_NAME_ABBR" db:"SecurityNameAbbr"` + DelistDate *CustomTime `json:"DELIST_DATE" db:"DelistDate"` + ListingDate *CustomTime `json:"LISTING_DATE" db:"ListingDate"` + ConvertStockCode string `json:"CONVERT_STOCK_CODE" db:"ConvertStockCode"` + BondExpire string `json:"BOND_EXPIRE" db:"BondExpire"` + Rating string `json:"RATING" db:"Rating"` + ValueDate CustomTime `json:"VALUE_DATE" db:"ValueDate"` + IssueYear string `json:"ISSUE_YEAR" db:"IssueYear"` + CeaseDate CustomTime `json:"CEASE_DATE" db:"CeaseDate"` + ExpireDate CustomTime `json:"EXPIRE_DATE" db:"ExpireDate"` + PayInterestDay string `json:"PAY_INTEREST_DAY" db:"PayInterestDay"` + InterestRateExplain string `json:"INTEREST_RATE_EXPLAIN" db:"InterestRateExplain"` + BondCombineCode string `json:"BOND_COMBINE_CODE" db:"BondCombineCode"` + ActualIssueScale float64 `json:"ACTUAL_ISSUE_SCALE" db:"ActualIssueScale"` + IssuePrice float64 `json:"ISSUE_PRICE" db:"IssuePrice"` + Remark string `json:"REMARK" db:"Remark"` + ParValue float64 `json:"PAR_VALUE" db:"ParValue"` + IssueObject string `json:"ISSUE_OBJECT" db:"IssueObject"` + RedeemType *string `json:"REDEEM_TYPE" db:"RedeemType"` + ExecuteReasonHS *string `json:"EXECUTE_REASON_HS" db:"ExecuteReasonHS"` + NoticeDateHS *CustomTime `json:"NOTICE_DATE_HS" db:"NoticeDateHS"` + NoticeDateSH *CustomTime `json:"NOTICE_DATE_SH" db:"NoticeDateSH"` + ExecutePriceHS *float64 `json:"EXECUTE_PRICE_HS" db:"ExecutePriceHS"` + ExecutePriceSH *float64 `json:"EXECUTE_PRICE_SH" db:"ExecutePriceSH"` + RecordDateSH *CustomTime `json:"RECORD_DATE_SH" db:"RecordDateSH"` + ExecuteStartDateSH *CustomTime `json:"EXECUTE_START_DATESH" db:"ExecuteStartDateSH"` + ExecuteStartDateHS *CustomTime `json:"EXECUTE_START_DATEHS" db:"ExecuteStartDateHS"` + ExecuteEndDate *CustomTime `json:"EXECUTE_END_DATE" db:"ExecuteEndDate"` + CorreCode string `json:"CORRECODE" db:"CorreCode"` + CorreCodeNameAbbr string `json:"CORRECODE_NAME_ABBR" db:"CorreCodeNameAbbr"` + PublicStartDate CustomTime `json:"PUBLIC_START_DATE" db:"PublicStartDate"` + CorreCodeO string `json:"CORRECODEO" db:"CorreCodeO"` + CorreCodeNameAbbrO string `json:"CORRECODE_NAME_ABBRO" db:"CorreCodeNameAbbrO"` + BondStartDate CustomTime `json:"BOND_START_DATE" db:"BondStartDate"` + SecurityStartDate CustomTime `json:"SECURITY_START_DATE" db:"SecurityStartDate"` + SecurityShortName string `json:"SECURITY_SHORT_NAME" db:"SecurityShortName"` + FirstPerPreplacing float64 `json:"FIRST_PER_PREPLACING" db:"FirstPerPreplacing"` + OnlineGeneralAAU float64 `json:"ONLINE_GENERAL_AAU" db:"OnlineGeneralAAU"` + OnlineGeneralLWR float64 `json:"ONLINE_GENERAL_LWR" db:"OnlineGeneralLWR"` + InitialTransferPrice float64 `json:"INITIAL_TRANSFER_PRICE" db:"InitialTransferPrice"` + TransferEndDate CustomTime `json:"TRANSFER_END_DATE" db:"TransferEndDate"` + TransferStartDate CustomTime `json:"TRANSFER_START_DATE" db:"TransferStartDate"` + ResaleClause string `json:"RESALE_CLAUSE" db:"ResaleClause"` + RedeemClause string `json:"REDEEM_CLAUSE" db:"RedeemClause"` + PartyName string `json:"PARTY_NAME" db:"PartyName"` + ConvertStockPrice interface{} `json:"CONVERT_STOCK_PRICE" db:"ConvertStockPrice"` + TransferPrice float64 `json:"TRANSFER_PRICE" db:"TransferPrice"` + TransferValue float64 `json:"TRANSFER_VALUE" db:"TransferValue"` + CurrentBondPrice interface{} `json:"CURRENT_BOND_PRICE" db:"CurrentBondPrice"` + TransferPremiumRatio float64 `json:"TRANSFER_PREMIUM_RATIO" db:"TransferPremiumRatio"` + ConvertStockPriceHQ *float64 `json:"CONVERT_STOCK_PRICEHQ" db:"ConvertStockPriceHQ"` + Market *string `json:"MARKET" db:"Market"` + ResaleTrigPrice float64 `json:"RESALE_TRIG_PRICE" db:"ResaleTrigPrice"` + RedeemTrigPrice float64 `json:"REDEEM_TRIG_PRICE" db:"RedeemTrigPrice"` + PBVRatio float64 `json:"PBV_RATIO" db:"PBVRatio"` + IBStartDate CustomTime `json:"IB_START_DATE" db:"IBStartDate"` + IBEndDate CustomTime `json:"IB_END_DATE" db:"IBEndDate"` + CashflowDate CustomTime `json:"CASHFLOW_DATE" db:"CashflowDate"` + CouponIR float64 `json:"COUPON_IR" db:"CouponIR"` + ParamName string `json:"PARAM_NAME" db:"ParamName"` + IssueType string `json:"ISSUE_TYPE" db:"IssueType"` + ExecuteReasonSH *string `json:"EXECUTE_REASON_SH" db:"ExecuteReasonSH"` + PaydayNew string `json:"PAYDAYNEW" db:"PaydayNew"` + CurrentBondPriceNew interface{} `json:"CURRENT_BOND_PRICENEW" db:"CurrentBondPriceNew"` + IsConvertStock string `json:"IS_CONVERT_STOCK" db:"IsConvertStock"` + IsRedeem string `json:"IS_REDEEM" db:"IsRedeem"` + IsSellback string `json:"IS_SELLBACK" db:"IsSellback"` + FirstProfit *float64 `json:"FIRST_PROFIT" db:"FirstProfit"` +} + +type CustomTime struct { + time.Time +} + +// 实现 driver.Valuer 接口 +func (ct CustomTime) Value() (driver.Value, error) { + return ct.Time, nil +} + +// 实现 sql.Scanner 接口 +func (ct *CustomTime) Scan(value interface{}) error { + if value == nil { + ct.Time = time.Time{} + return nil + } + switch v := value.(type) { + case time.Time: + ct.Time = v + return nil + default: + return fmt.Errorf("cannot scan type %T into CustomTime", value) + } +} + +// 实现 UnmarshalJSON 方法以支持自定义时间格式 +func (ct *CustomTime) UnmarshalJSON(b []byte) error { + str := string(b) + str = str[1 : len(str)-1] // 去掉引号 + + if str == "null" { + return nil + } + + // 使用正确的时间格式进行解析 + parsedTime, err := time.Parse("2006-01-02 15:04:05", str) + if err != nil { + return err + } + + ct.Time = parsedTime + return nil +} + +type BondResponse struct { + Version string `json:"version"` + Result BondResult `json:"result"` + Success bool `json:"success"` + Message string `json:"message"` + Code int `json:"code"` +} + +type BondResult struct { + Pages int `json:"pages"` + Data []BondData `json:"data"` + Count int `json:"count"` +} diff --git a/handler/newbond/newbond.go b/handler/newbond/newbond.go new file mode 100644 index 0000000..7aa0bb9 --- /dev/null +++ b/handler/newbond/newbond.go @@ -0,0 +1,44 @@ +package newbond + +import ( + "git.lxtend.com/qqbot/constants" + "git.lxtend.com/qqbot/handler" + "git.lxtend.com/qqbot/model" +) + +func init() { + handler.RegisterHandler("监听新债", listenBond, constants.LEVEL_USER) + handler.RegisterHelpInform("监听新债", "监听新债 开启新债监听") + handler.RegisterHandler("取消监听新债", unListenBond, constants.LEVEL_USER) + handler.RegisterHelpInform("取消监听新债", "取消监听新债 关闭新债监听") +} + +func listenBond(msg model.Message) (reply model.Reply) { + if err := AddGroupListen(int(msg.GroupInfo.GroupId)); err != nil { + return model.Reply{ + ReplyMsg: "开启新债监听失败,报错: " + err.Error(), + ReferOriginMsg: true, + FromMsg: msg, + } + } + return model.Reply{ + ReplyMsg: "开启新债监听成功", + ReferOriginMsg: true, + FromMsg: msg, + } +} + +func unListenBond(msg model.Message) (reply model.Reply) { + if err := RemoveGroupListen(int(msg.GroupInfo.GroupId)); err != nil { + return model.Reply{ + ReplyMsg: "关闭新债监听失败,报错: " + err.Error(), + ReferOriginMsg: true, + FromMsg: msg, + } + } + return model.Reply{ + ReplyMsg: "关闭新债监听成功", + ReferOriginMsg: true, + FromMsg: msg, + } +} diff --git a/handler/newbond/service.go b/handler/newbond/service.go new file mode 100644 index 0000000..a59aedf --- /dev/null +++ b/handler/newbond/service.go @@ -0,0 +1,267 @@ +package newbond + +import ( + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "git.lxtend.com/qqbot/action" + "git.lxtend.com/qqbot/model" + "git.lxtend.com/qqbot/sqlite3" +) + +func init() { + createNewBondListenGroupTableSQL := ` + CREATE TABLE IF NOT EXISTS new_bond_listen_group ( + group_id INT PRIMARY KEY + ); + ` + createNewBondInfoTableSQL := `CREATE TABLE BondData ( + SecurityCode VARCHAR(10) NOT NULL, + SecuCode VARCHAR(15) NOT NULL, + TradeMarket VARCHAR(10) NOT NULL, + SecurityNameAbbr VARCHAR(50) NOT NULL, + DelistDate DATETIME NULL, + ListingDate DATETIME NULL, + ConvertStockCode VARCHAR(10) NOT NULL, + BondExpire VARCHAR(5) NOT NULL, + Rating VARCHAR(5) NOT NULL, + ValueDate DATETIME NOT NULL, + IssueYear VARCHAR(4) NOT NULL, + CeaseDate DATETIME NOT NULL, + ExpireDate DATETIME NOT NULL, + PayInterestDay VARCHAR(10) NOT NULL, + InterestRateExplain TEXT NOT NULL, + BondCombineCode VARCHAR(20) NOT NULL, + ActualIssueScale DECIMAL(10, 2) NOT NULL, + IssuePrice DECIMAL(10, 2) NOT NULL, + Remark TEXT NOT NULL, + ParValue DECIMAL(10, 2) NOT NULL, + IssueObject TEXT NOT NULL, + RedeemType VARCHAR(50) NULL, + ExecuteReasonHS VARCHAR(255) NULL, + NoticeDateHS DATETIME NULL, + NoticeDateSH DATETIME NULL, + ExecutePriceHS DECIMAL(10, 2) NULL, + ExecutePriceSH DECIMAL(10, 2) NULL, + RecordDateSH DATETIME NULL, + ExecuteStartDateSH DATETIME NULL, + ExecuteStartDateHS DATETIME NULL, + ExecuteEndDate DATETIME NULL, + CorreCode VARCHAR(10) NOT NULL, + CorreCodeNameAbbr VARCHAR(50) NOT NULL, + PublicStartDate DATETIME NOT NULL, + CorreCodeO VARCHAR(10) NOT NULL, + CorreCodeNameAbbrO VARCHAR(50) NOT NULL, + BondStartDate DATETIME NOT NULL, + SecurityStartDate DATETIME NOT NULL, + SecurityShortName VARCHAR(50) NOT NULL, + FirstPerPreplacing DECIMAL(10, 4) NOT NULL, + OnlineGeneralAAU DECIMAL(10, 2) NOT NULL, + OnlineGeneralLWR DECIMAL(20, 10) NOT NULL, + InitialTransferPrice DECIMAL(10, 2) NOT NULL, + TransferEndDate DATETIME NOT NULL, + TransferStartDate DATETIME NOT NULL, + ResaleClause TEXT NOT NULL, + RedeemClause TEXT NOT NULL, + PartyName VARCHAR(100) NOT NULL, + ConvertStockPrice DECIMAL(10, 2) NOT NULL, + TransferPrice DECIMAL(10, 2) NOT NULL, + TransferValue DECIMAL(10, 4) NOT NULL, + CurrentBondPrice VARCHAR(10) NOT NULL, + TransferPremiumRatio DECIMAL(10, 2) NOT NULL, + ConvertStockPriceHQ DECIMAL(10, 2) NULL, + Market VARCHAR(50) NULL, + ResaleTrigPrice DECIMAL(10, 2) NOT NULL, + RedeemTrigPrice DECIMAL(10, 2) NOT NULL, + PBVRatio DECIMAL(10, 2) NOT NULL, + IBStartDate DATETIME NOT NULL, + IBEndDate DATETIME NOT NULL, + CashflowDate DATETIME NOT NULL, + CouponIR DECIMAL(10, 2) NOT NULL, + ParamName TEXT NOT NULL, + IssueType VARCHAR(10) NOT NULL, + ExecuteReasonSH VARCHAR(255) NULL, + PaydayNew VARCHAR(10) NOT NULL, + CurrentBondPriceNew DECIMAL(10, 2) NOT NULL, + IsConvertStock VARCHAR(10) NOT NULL, + IsRedeem VARCHAR(10) NOT NULL, + IsSellback VARCHAR(10) NOT NULL, + FirstProfit DECIMAL(10, 2) NULL, + PRIMARY KEY (SecurityCode) + ); + ` + sqlite3.TryCreateTable(createNewBondListenGroupTableSQL) + sqlite3.TryCreateTable(createNewBondInfoTableSQL) + go RoundCheckNewBond() +} + +func GetBondsData() ([]BondData, error) { + url := "https://datacenter-web.eastmoney.com/api/data/v1/get?sortColumns=PUBLIC_START_DATE%2CSECURITY_CODE&sortTypes=-1%2C-1&pageSize=50&pageNumber=1&reportName=RPT_BOND_CB_LIST&columns=ALL"eColumns=f2~01~CONVERT_STOCK_CODE~CONVERT_STOCK_PRICE%2Cf235~10~SECURITY_CODE~TRANSFER_PRICE%2Cf236~10~SECURITY_CODE~TRANSFER_VALUE%2Cf2~10~SECURITY_CODE~CURRENT_BOND_PRICE%2Cf237~10~SECURITY_CODE~TRANSFER_PREMIUM_RATIO%2Cf239~10~SECURITY_CODE~RESALE_TRIG_PRICE%2Cf240~10~SECURITY_CODE~REDEEM_TRIG_PRICE%2Cf23~01~CONVERT_STOCK_CODE~PBV_RATIO"eType=0&source=WEB&client=WEB" + + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("status code %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var apiResponse BondResponse + err = json.Unmarshal(body, &apiResponse) + if err != nil { + return nil, err + } + + return apiResponse.Result.Data, nil +} + +func AddGroupListen(groupID int) error { + tx, err := sqlite3.GetTran() + if err != nil { + return err + } + defer tx.Rollback() + tx.Exec("INSERT INTO new_bond_listen_group (group_id) VALUES (?)", groupID) + if err := tx.Commit(); err != nil { + return err + } + return nil +} + +func RemoveGroupListen(groupID int) error { + tx, err := sqlite3.GetTran() + if err != nil { + return err + } + defer tx.Rollback() + tx.Exec("DELETE FROM new_bond_listen_group WHERE group_id = ?", groupID) + if err := tx.Commit(); err != nil { + return err + } + return nil +} + +func BondDataExists(securityCode string) (bool, error) { + db := sqlite3.GetDB() + var count int + if err := db.Get(&count, "SELECT COUNT(*) FROM BondData WHERE SecurityCode = ?", securityCode); err != nil { + return false, err + } + return count > 0, nil +} + +func AddBondData(data BondData) error { + tx, err := sqlite3.GetTran() + if err != nil { + return err + } + defer tx.Rollback() + query := `INSERT INTO BondData ( + SecurityCode, SecuCode, TradeMarket, SecurityNameAbbr, DelistDate, + ListingDate, ConvertStockCode, BondExpire, Rating, ValueDate, + IssueYear, CeaseDate, ExpireDate, PayInterestDay, InterestRateExplain, + BondCombineCode, ActualIssueScale, IssuePrice, Remark, ParValue, + IssueObject, RedeemType, ExecuteReasonHS, NoticeDateHS, NoticeDateSH, + ExecutePriceHS, ExecutePriceSH, RecordDateSH, ExecuteStartDateSH, ExecuteStartDateHS, + ExecuteEndDate, CorreCode, CorreCodeNameAbbr, PublicStartDate, CorreCodeO, + CorreCodeNameAbbrO, BondStartDate, SecurityStartDate, SecurityShortName, FirstPerPreplacing, + OnlineGeneralAAU, OnlineGeneralLWR, InitialTransferPrice, TransferEndDate, TransferStartDate, + ResaleClause, RedeemClause, PartyName, ConvertStockPrice, TransferPrice, + TransferValue, CurrentBondPrice, TransferPremiumRatio, ConvertStockPriceHQ, Market, + ResaleTrigPrice, RedeemTrigPrice, PBVRatio, IBStartDate, IBEndDate, + CashflowDate, CouponIR, ParamName, IssueType, ExecuteReasonSH, + PaydayNew, CurrentBondPriceNew, IsConvertStock, IsRedeem, IsSellback, + FirstProfit + ) VALUES ( + :SecurityCode, :SecuCode, :TradeMarket, :SecurityNameAbbr, :DelistDate, + :ListingDate, :ConvertStockCode, :BondExpire, :Rating, :ValueDate, + :IssueYear, :CeaseDate, :ExpireDate, :PayInterestDay, :InterestRateExplain, + :BondCombineCode, :ActualIssueScale, :IssuePrice, :Remark, :ParValue, + :IssueObject, :RedeemType, :ExecuteReasonHS, :NoticeDateHS, :NoticeDateSH, + :ExecutePriceHS, :ExecutePriceSH, :RecordDateSH, :ExecuteStartDateSH, :ExecuteStartDateHS, + :ExecuteEndDate, :CorreCode, :CorreCodeNameAbbr, :PublicStartDate, :CorreCodeO, + :CorreCodeNameAbbrO, :BondStartDate, :SecurityStartDate, :SecurityShortName, :FirstPerPreplacing, + :OnlineGeneralAAU, :OnlineGeneralLWR, :InitialTransferPrice, :TransferEndDate, :TransferStartDate, + :ResaleClause, :RedeemClause, :PartyName, :ConvertStockPrice, :TransferPrice, + :TransferValue, :CurrentBondPrice, :TransferPremiumRatio, :ConvertStockPriceHQ, :Market, + :ResaleTrigPrice, :RedeemTrigPrice, :PBVRatio, :IBStartDate, :IBEndDate, + :CashflowDate, :CouponIR, :ParamName, :IssueType, :ExecuteReasonSH, + :PaydayNew, :CurrentBondPriceNew, :IsConvertStock, :IsRedeem, :IsSellback, + :FirstProfit + )` + + _, err = tx.NamedExec(query, data) + if err != nil { + return err + } + if err := tx.Commit(); err != nil { + return err + } + return nil +} + +func GetGroupListens() ([]int, error) { + db := sqlite3.GetDB() + var groupIDs []int + if err := db.Select(&groupIDs, "SELECT group_id FROM new_bond_listen_group"); err != nil && err != sql.ErrNoRows { + return nil, err + } + return groupIDs, nil +} + +func RoundCheckNewBond() { + time.Sleep(5 * time.Second) + for !action.ActionManager.Started() { + time.Sleep(5 * time.Second) + } + once := true + for { + if !once { + time.Sleep(5 * time.Minute) + } + bonds, err := GetBondsData() + if bonds == nil || err != nil { + fmt.Println("Error getting bonds data:", err) + continue + } + groups, err := GetGroupListens() + if err != nil { + fmt.Println("Error getting group listens:", err) + continue + } + for _, bond := range bonds { + exists, err := BondDataExists(bond.SecurityCode) + if err != nil { + fmt.Println("Error checking bond data exists:", err) + continue + } + if !exists && bond.ListingDate == nil { + for _, group := range groups { + msg := model.Reply{ + ReplyMsg: fmt.Sprintf("号外号外,%s开始申购了", bond.SecurityNameAbbr), + ReferOriginMsg: false, + FromMsg: model.Message{ + GroupInfo: model.GroupInfo{ + GroupId: int64(group), + }, + }, + } + action.ActionManager.SendMsg(msg) + } + } + AddBondData(bond) + } + once = false + } +} diff --git a/register.go b/register.go index 20f46f0..60bfdca 100644 --- a/register.go +++ b/register.go @@ -12,6 +12,7 @@ import ( _ "git.lxtend.com/qqbot/handler/help" _ "git.lxtend.com/qqbot/handler/jrrp" _ "git.lxtend.com/qqbot/handler/kw" + _ "git.lxtend.com/qqbot/handler/newbond" _ "git.lxtend.com/qqbot/handler/restart" _ "git.lxtend.com/qqbot/handler/roll" _ "git.lxtend.com/qqbot/handler/scoresaber"