Skip to content

Instantly share code, notes, and snippets.

@athurg
Created September 28, 2016 11:38
Show Gist options
  • Save athurg/349c939b7c0b1d527160a0c2b691d5db to your computer and use it in GitHub Desktop.
Save athurg/349c939b7c0b1d527160a0c2b691d5db to your computer and use it in GitHub Desktop.
国网四川省电力公司电费查询工具
/*
掌上川电客户端
@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)
}
@Anduin2017
Copy link

好东西!是时候让我拿去开发一个自动电费余额报警了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment