Created
September 28, 2016 11:38
-
-
Save athurg/349c939b7c0b1d527160a0c2b691d5db to your computer and use it in GitHub Desktop.
国网四川省电力公司电费查询工具
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
掌上川电客户端 | |
@description 可以查询用户信息近期用电详情 | |
使用方式: | |
1. 先以`-listen port`参数启动本程序,建立一个HTTP代理服务器 | |
2. 设置手机使用这个代理服务器,并打开“掌上川电”客户端进行登陆 | |
3. 登陆成功后,本代理服务器会自动检测到会话编号并输出,此时可以关闭手机代理的设置 | |
4. 以`-sid 会话编号`为参数启动本程序,即可查询 | |
TODO: 自动登陆并获取SessionID | |
目前抓包可分析,用户名186xxxxxxxx密码186xxxxxxxx的账户,登陆请求如下: | |
` | |
curl "http://222.212.254.79/appv2/services/rest/tokenlogin" \ | |
-H "token:c05352d04a8ece5a5f094452abd658ee1cfea78a0aa7efbb5c9c681d9bf8d7e5" \ | |
-H "Phonetype:iPhone" \ | |
-H "Verifycode:" \ | |
-H "Accept-Encoding:gzip" \ | |
-H "Connection:keep-alive" \ | |
-H "Id:J3X9o7fmLfzaxdZgFzNwJw==" \ | |
-H "Pwd:J3X9o7fmLfzaxdZgFzNwJw==" | |
` | |
已知: | |
1. Verifycode验证码,一般不用,密码错误时必须 | |
2. Id、Pwd分别表示用户名密码,加密算法未知 | |
3. Token每次登陆都一样,不知道怎么生成的 | |
4. 响应的数据每次都不同,也是加密的 | |
常用接口如下: | |
1. his/fee : 缴费记录 | |
2. his/read : 历史月份读表记录 | |
3. meter : 本月每天读表记录 | |
4. cons : 绑定电卡信息 | |
*/ | |
package main | |
import ( | |
"encoding/xml" | |
"flag" | |
"fmt" | |
"io" | |
"io/ioutil" | |
"log" | |
"net/http" | |
"os" | |
"sort" | |
"time" | |
) | |
const ( | |
URI = "http://222.212.254.79/appv2/services/rest/" | |
SessionID = "7a914124-7113-4dce-bae3-6eadaa836108" | |
) | |
type Meter struct { | |
TheDate int `xml:"thedate"` | |
Duration float32 | |
Total float32 `xml:"total"` //表读数,要计算当月度数需要前后两月相减 | |
Read4 float32 `xml:"read4"` | |
Read3 float32 `xml:"read3"` | |
Read2 float32 `xml:"read2"` | |
} | |
func (m Meter) String() string { | |
return fmt.Sprintf("%d: %8.2f度 (读数:总%8.2f 峰%8.2f 平%8.2f 谷%8.2f)", m.TheDate, m.Duration, m.Total, m.Read2, m.Read3, m.Read4) | |
} | |
type MeterSlice []Meter | |
func (p MeterSlice) Len() int { return len(p) } | |
func (p MeterSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } | |
func (p MeterSlice) Less(i, j int) bool { return p[i].TheDate < p[j].TheDate } | |
//电卡信息 | |
type Consumer struct { | |
ID string `xml:"consno"` //电卡户号 | |
Name string `xml:"consname"` //电卡户名 | |
CorpID string `xml:"orgno"` //供电公司编号 | |
Address string `xml:"address"` //客户地址 | |
AccountId string `xml:"accountid"` //用户登陆账户 | |
PrepayMode string `xml:"prepaymode"` //电表模式02=>插卡电表 | |
Bindingdate string `xml:"bindingdate"` //电卡绑定时间 | |
RequestStatus string `xml:"request_status"` //请求状态 | |
} | |
type ConsumerSlice []Consumer | |
func (p ConsumerSlice) Len() int { return len(p) } | |
func (p ConsumerSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } | |
func (p ConsumerSlice) Less(i, j int) bool { return p[i].ID < p[j].ID } | |
//阶梯价格信息 | |
type Price struct { | |
KwhPrice float32 `xml:"kwh_prc"` | |
PriceTiCode string `xml:"prc_ti_code"` //峰平谷 | |
RangeTypeCode string `xml:"range_type_code"` //31代表181-280度加价,32代表180以上加价 | |
} | |
type PriceSlice []Price | |
//缴费记录 | |
type PayFlow struct { | |
ChargeYearMonth string `xml:"charge_ym"` | |
ChargeDate string `xml:"charge_date"` | |
ConsumerId string `xml:"cons_no"` | |
OrganizationID string `xml:"org_no"` | |
Amount int `xml:"rcv_amt"` //价格 | |
} | |
func (pf PayFlow) String() string { | |
return fmt.Sprintf("年月%s: 时间: %s, 金额:%-4d", pf.ChargeYearMonth, pf.ChargeDate, pf.Amount) | |
} | |
//读表信息 | |
type Remand struct { | |
Money float32 `xml:"money"` | |
MeterID string `xml:"meterno"` | |
BuyTimes int `xml:"buytimes"` | |
UpdateAt int `xml:"thedate"` | |
} | |
type PayFlowSlice []PayFlow | |
func (p PayFlowSlice) Len() int { return len(p) } | |
func (p PayFlowSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } | |
func (p PayFlowSlice) Less(i, j int) bool { return p[i].ChargeDate < p[j].ChargeDate } | |
type ResponseInfo struct { | |
Result string `xml:"result"` | |
consumerId string `xml:"consno,omitempty"` | |
ConsumerNo string `xml:"cons_no,omitempty"` | |
Remand Remand `xml:"remand,omitempty"` | |
Meters MeterSlice `xml:"meter>entity,omitempty"` | |
Prices PriceSlice `xml:"price>entity,omitempty"` | |
PayFlows PayFlowSlice `xml:"pay_flow>entity,omitempty"` | |
Consumers ConsumerSlice `xml:"consnos>entity,omitempty"` | |
} | |
//执行川电API接口查询 | |
//川电API所有的参数都是通过Header进行传递 | |
func QueryInfo(path string, params map[string]string) (info ResponseInfo, err error) { | |
request, err := http.NewRequest("GET", URI+path, nil) | |
for k, v := range params { | |
request.Header.Set(k, v) | |
} | |
response, err := http.DefaultClient.Do(request) | |
if err != nil { | |
return | |
} | |
defer response.Body.Close() | |
data, err := ioutil.ReadAll(response.Body) | |
if err != nil { | |
return | |
} | |
err = xml.Unmarshal(data, &info) | |
if err != nil { | |
log.Println("无法解析的数据:", string(data)) | |
} | |
return | |
} | |
//查询当月每天用量 | |
func DumpMeter(consumerId, sessionId string, fromDate time.Time) { | |
params := map[string]string{ | |
"Dtype": "0 1 2 4", //TODO:Dtype干什么用的? | |
"session_key": sessionId, | |
"Thedate": fromDate.Format("20060102"), | |
} | |
info, err := QueryInfo("meter/"+consumerId, params) | |
if err != nil { | |
log.Println("查询当月电量错误", err) | |
return | |
} | |
log.Println("当前账户信息:") | |
log.Printf(" 购电次数:%-3d", info.Remand.BuyTimes) | |
log.Printf(" 当前余额:%.2f", info.Remand.Money) | |
log.Printf(" 读表时间:%d", info.Remand.UpdateAt) | |
log.Printf(" 读表序号:%s", info.Remand.MeterID) | |
sort.Sort(info.Meters) | |
var prevMeter Meter | |
if len(info.Meters) > 0 { | |
prevMeter = info.Meters[0] | |
} | |
log.Println("本月电量读数:", info.Result) | |
for _, meter := range info.Meters { | |
meter.Duration = meter.Total - prevMeter.Total | |
log.Println("\t" + meter.String()) | |
prevMeter = meter | |
} | |
} | |
//查历史每月用量 | |
func DumpHistory(consumerId, sessionId string, fromDate time.Time) { | |
params := map[string]string{ | |
"session_key": SessionID, | |
"Thedate": fromDate.Format("20060102"), | |
} | |
info, err := QueryInfo("his/read/"+consumerId, params) | |
if err != nil { | |
log.Println("历史电量查询错误", err) | |
return | |
} | |
sort.Sort(info.Meters) | |
var prevMeter Meter | |
if len(info.Meters) > 0 { | |
prevMeter = info.Meters[0] | |
} | |
log.Println("历史电量读数:") | |
for _, meter := range info.Meters { | |
meter.Duration = meter.Total - prevMeter.Total | |
log.Println("\t" + meter.String()) | |
prevMeter = meter | |
} | |
} | |
//查缴费记录 | |
func DumpPayFlow(consumerId, sessionId string) { | |
params := map[string]string{"session_key": SessionID, "Ftype": "N"} | |
info, err := QueryInfo("his/fee/"+consumerId, params) | |
if err != nil { | |
log.Println("缴费列表查询错误", err) | |
return | |
} | |
sort.Sort(info.PayFlows) | |
log.Println("缴费列表") | |
for _, payFlow := range info.PayFlows { | |
log.Println("\t", payFlow) | |
} | |
} | |
func main() { | |
var proxyPort int | |
var sessionId string | |
var logFileName string | |
flag.IntVar(&proxyPort, "listen", 0, "是否启动代理模式") | |
flag.StringVar(&sessionId, "sid", "88c6b915-46b8-4425-8020-4febc0912f3d", "掌上川电通信的会话号") | |
flag.StringVar(&logFileName, "log", "stdout", "日志文件") | |
flag.Parse() | |
if proxyPort > 0 { | |
startHttpProxyServer(proxyPort) | |
return | |
} | |
if logFileName!="stdout" { | |
if logFileName=="timestamp" { | |
logFileName = "/tmp/"+time.Now().Format("2006-01-02_15")+".log" | |
} | |
logFile, err := os.OpenFile(logFileName, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666) | |
if err != nil { | |
log.Println("打开日志文件错误", err) | |
} else { | |
log.SetOutput(logFile) | |
} | |
} | |
info, err := QueryInfo("cons", map[string]string{"session_key": sessionId}) | |
if err != nil { | |
log.Println("客户信息查询失败:", err) | |
return | |
} | |
if info.Result != "succeed" { | |
log.Println("客户信息查询失败:", info.Result) | |
return | |
} | |
sort.Sort(info.Consumers) | |
for _, consumer := range info.Consumers { | |
log.Println("") | |
log.Println("---------------------------------------------------------------------------") | |
log.Println(consumer.ID, consumer.Name) | |
DumpPayFlow(consumer.ID, sessionId) | |
DumpHistory(consumer.ID, sessionId, time.Now().AddDate(-1, 0, 0)) | |
DumpMeter(consumer.ID, sessionId, time.Now().AddDate(0, -1, 0)) | |
} | |
} | |
func startHttpProxyServer(port int) { | |
//启动HTTP转发,如果获取到掌上川电请求用户信息的借口时,输出会话编号 | |
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | |
log.Println("请求:", r.URL.String()) | |
if r.URL.Host=="115.28.164.191" { | |
r.URL.Host = "172.20.100.73" | |
log.Println(r.URL.Path) | |
r.URL.Path = "/servers/list" | |
} | |
r.RequestURI = "" | |
resp, err := http.DefaultClient.Do(r) | |
if err != nil { | |
return | |
} | |
defer resp.Body.Close() | |
resp.Header = w.Header() | |
w.WriteHeader(resp.StatusCode) | |
n,err := io.Copy(w, resp.Body) | |
log.Println("响应",n,"字节", err) | |
if r.URL.String() == URI+"cons" { | |
log.Println("会话编号", r.Header.Get("session_key")) | |
} | |
}) | |
log.Println("HTTP代理服务器已启动, 监听端口", port) | |
http.ListenAndServe(fmt.Sprintf(":%d", port), nil) | |
os.Exit(0) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
好东西!是时候让我拿去开发一个自动电费余额报警了