Skip to content

Commit

Permalink
feature: update v2.0
Browse files Browse the repository at this point in the history
- 更新 电信请求接口。从容器化模拟登录,更换为纯接口调用登录
- 调整 日志打印规则
- 新增 /show/qryImportantData、/show/userFluxPackage 接口(需要启动应用时开启dev模式才能访问)
- 删除 /show/detail、/show/flowPackage 接口
- 删除 dockerProt、dockerWaitTime 启动参数
  • Loading branch information
LambdaExpression authored and LambdaExpression committed Apr 22, 2024
1 parent a7fa209 commit 8bb771f
Show file tree
Hide file tree
Showing 13 changed files with 927 additions and 1,219 deletions.
111 changes: 37 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,84 +4,32 @@

**中国电信 手机话费、流量、语音通话监控**

本工具是部署在服务器(或x86软路由等设备) 使用 docker [lambdaexpression/headless-shell-utf-8](https://hub.docker.com/r/lambdaexpression/headless-shell-utf-8) 进行模拟浏览器登录获取cookie,按需定时获取电信手机话费、流量、语音通话使用情况,通过接口返回数据。
本工具是部署在服务器(或x86软路由等设备) 使用 接口模拟登录获取cookie,按需定时获取电信手机话费、流量、语音通话使用情况,通过接口返回数据。
可配合 Scriptables 插件 [ChinaTelecomPanel](https://lambdaexpression.github.io/ScriptablesComponent/ChinaTelecomPanel/) 一起使用


### 版本更新

*(注:应用如果在正常期间突然无法使用,大概率是电信登录页面的页面元素发生改变,导致登录逻辑无法正常执行。在保证自身使用的是最新版本后,任然无法解决可通过 [Issuess](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues) 进行反馈。电信的这种行为,个人怀疑这是电信的防爬虫策略,现阶段应用也只能发现一种就通过版本迭代兼容一种)*


**v1.0.7.2**

update:
- 更新 电信页面解析逻辑

**v1.0.7.1**

update:
- 优化 docker容器连接

**v1.0.7**

add:
- 兼容 电信网页登录时,弹出的"当前环境不支持免密登录"提示。(因为这个弹框,旧版本基本都已经无法正常使用,建议都升级到最新版本)

**v1.0.6.1**

update:
- 优化 网页截图

**v1.0.6**

update:
- 优化 电信检查环境时间逻辑 [#9](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/9)

**v1.0.5**

add:
- 添加 /show/flowPackage 接口 [#8](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/8)
**v2.0**

update:
- 更新 通用流量和专用流量统计规则 [#8](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/8)

**v1.0.4**

add:
- 添加 -version 版本打印

update:

- 优化 自适应电信登录页面登录动作 [#5](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/5)

**v1.0.3**

add:
- 添加 /show/detail 接口 [#4](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/4)

**v1.0.2**

add:
- 添加自动判断是否勾选用户隐私协议 [#3](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/3)

**v1.0.1**

update:
- 修复新版本电信登录页面默认勾选隐私协议,导致登录失败问题 [#1](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/1)
- 更新 电信请求接口。从容器化模拟登录,更换为纯接口调用登录
- 调整 日志打印规则
- 新增 /show/qryImportantData、/show/userFluxPackage 接口(需要启动应用时开启dev模式才能访问)
- 删除 /show/detail、/show/flowPackage 接口
- 删除 dockerProt、dockerWaitTime 启动参数

### 1.准备

- 1.准备一个可正常登录[电信](https://e.189.cn/wap/index.do)账号密码
- 2.安装 docker (可自行查询谷哥或度娘)
- 3.执行 `docker pull lambdaexpression/headless-shell-utf-8:95.0.4638.32`,下载 [lambdaexpression/headless-shell-utf-8](https://hub.docker.com/r/lambdaexpression/headless-shell-utf-8) 容器到本地
- 4.下载本应用 `wget https://github.com/LambdaExpression/ChinaTelecomMonitor/releases/download/v1.0.6.1/China_Telecom_Monitor_amd64`
- 5.应用授权 `chmod +x ./China_Telecom_Monitor_amd64`
- 1.准备一个可正常登录 电信APP 的账号密码(**注意:电信APP 的密码 和以前的h5登录密码不是同一个**
- 2.下载本应用 `wget https://github.com/LambdaExpression/ChinaTelecomMonitor/releases/download/v2.0/China_Telecom_Monitor_amd64`
- 3.应用授权 `chmod +x ./China_Telecom_Monitor_amd64`

### 2.启动应用

```
$ ./China_Telecom_Monitor_amd64 --prot 8081 --dockerProt 9222 --username '电信账号' --password '电信密码'
$ ./China_Telecom_Monitor_amd64 --prot 8081 --username '电信账号' --password '电信密码'
```

### 3.测试访问
Expand Down Expand Up @@ -141,11 +89,7 @@ Usage of ./China_Telecom_Monitor_amd64:
-dataPath string
--dataPath ./data # 数据日志文件保存路径 (default "./data")
-dev
--dev false # 开发模式,开启后将支持以下接口: /refresh 手动更新流量,/loginLog 查看登录截图日志
-dockerProt string
--dockerProt 9222 #登录容器使用的端口 (default "9222")
-dockerWaitTime int
--dockerWaitTime 60 #登录容器等待启动时间 (default 60)
--dev false # 开发模式,开启后将支持以下接口: /refresh 手动更新流量 和 /show/qryImportantData /show/userFluxPackage 这里两个电信接口
-intervalsTime int
--intervalsTime 180 #接口防止重刷时间 (default 180)
-logEncoding string
Expand All @@ -169,15 +113,30 @@ Usage of ./China_Telecom_Monitor_amd64:

**额外接口说明**


```
curl http://127.0.0.1:8081/show/qryImportantData
```

这些接口主要提供给有二次开发需求的用户使用。接口的数据是从电信的 https://appfuwu.189.cn:9021/query/qryImportantData 接口获取而来,没有进行数据二次处理,完全是原始数据输出。[#4](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/4)


```
curl http://127.0.0.1:8081/show/userFluxPackage
```

这些接口主要提供给有二次开发需求的用户使用。接口的数据是从电信的 https://appfuwu.189.cn:9021/query/userFluxPackage 接口获取而来,没有进行数据二次处理,完全是原始数据输出。[#4](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/4)


```
curl http://127.0.0.1:8081/show/detail
curl http://127.0.0.1:8081/show/detail (已无法使用)
```

这些接口主要提供给有二次开发需求的用户使用。接口的数据是从电信的 https://e.189.cn/store/user/package_detail.do 接口获取而来,没有进行数据二次处理,完全是原始数据输出。[#4](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/4)


```
curl http://127.0.0.1:8081/show/flowPackage
curl http://127.0.0.1:8081/show/flowPackage (已无法使用)
```

接口数据来源自 https://e.189.cn/store/wap/flowPackage.do ,数据同样不进行二次处理,原样输出 [#8](https://github.com/LambdaExpression/ChinaTelecomMonitor/issues/8#issuecomment-1165158557)
Expand All @@ -188,12 +147,16 @@ curl http://127.0.0.1:8081/show/flowPackage

下面是结合 `nohup` 后的启动命令,这样就能保证大家退出服务器后,服务仍然在运行
```
$ nohup ./China_Telecom_Monitor_amd64 --prot 8081 --dockerProt 9222 --username '电信账号' --password '电信密码' >/dev/null &
$ nohup ./China_Telecom_Monitor_amd64 --prot 8081 --username '电信账号' --password '电信密码' >/dev/null &
```

**异常情况**

如果上述第3步,返回数据有问题或无法访问。
- 1. 先查看一下同级目录下的 `./data/login/01.png``./data/login/02.png``./data/login/03.png` 这三张截图,是否有存在账号密码错误、登录要求输入验证码等情况。处理“登录要求输入验证码”,需要先手动访问 [https://e.189.cn/wap/index.do](https://e.189.cn/wap/index.do) 进行一次正常登陆,正常情况下就能恢复了。
- 2. 如果上述截图无法找到问题,可以查看一下 `./data/log/stdout.log` 是否存在异常信息(注意:首次启动时提示 `open ./data/cookie.json: no such file or directory` 是正常情况)

```json
{"headerInfos":{"code":"0000","reason":"操作成功"},"responseData":{"resultCode":"1000","resultDesc":"请求失败","attach":"","data":null}}
```
遇到“请求失败”,请确保在 **手机电信APP** 内测试一下登录账号密码是否正确


### 最后感谢 [boxjs](https://github.com/gsons/boxjs) 项目开源提供的电信接口
133 changes: 54 additions & 79 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@ import (
"China_Telecom_Monitor/tools"
"flag"
"fmt"
"github.com/LambdaExpression/surf/agent"
"github.com/LambdaExpression/surf/surf"
"github.com/golang-module/carbon/v2"
"github.com/kataras/iris/v12"
"github.com/robfig/cron/v3"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"strings"
"time"
)

var Version = "v1.0.7.2"
var Version = "v2.0"
var GoVersion = "not set"
var GitCommit = "not set"
var BuildTime = "not set"
Expand All @@ -36,7 +34,6 @@ func main() {
return
}

initBrowser()
initCron()

// 初始化流量获取
Expand All @@ -48,20 +45,18 @@ func main() {
// 初始化配置
func initFlag() {
flag.StringVar(&configs.Prot, "prot", "8080", "--prot 8080")
flag.StringVar(&configs.DockerProt, "dockerProt", "9222", "--dockerProt 9222 #登录容器使用的端口")
flag.StringVar(&configs.Username, "username", "", "--username 1xxxxxxxxxx #电信账号用户名, 必填")
flag.StringVar(&configs.Password, "password", "", "--password xxxxx #电信账号密码, 必填")
flag.IntVar(&configs.LoginIntervalTime, "loginIntervalTime", 43200, "--loginIntervalTime 43200 #电信登录间隔时间(防止被封号),秒")
flag.Int64Var(&configs.TimeOut, "timeOut", 30, "--timeOut 30 #访问电信接口请求超时时间,秒")
flag.IntVar(&configs.IntervalsTime, "intervalsTime", 180, "--intervalsTime 180 #接口防止重刷时间")
flag.IntVar(&configs.DockerWaitTime, "dockerWaitTime", 60, "--dockerWaitTime 60 #登录容器等待启动时间")

flag.StringVar(&configs.LogLevel, "logLevel", "info", "--logLevel info # 日志等级")
flag.StringVar(&configs.LogEncoding, "logEncoding", "console", "--logEncoding console # 日志输出格式 console 或 json")

flag.StringVar(&configs.DataPath, "dataPath", "./data", "--dataPath ./data # 数据日志文件保存路径")

flag.BoolVar(&configs.Dev, "dev", false, "--dev false # 开发模式,开启后将支持以下接口: /refresh 手动更新流量,/loginLog 查看登录截图日志")
flag.BoolVar(&configs.Dev, "dev", false, "--dev false # 开发模式,开启后将支持以下接口: /refresh 手动更新流量 和 /show/qryImportantData /show/userFluxPackage 这里两个电信接口")
flag.BoolVar(&configs.PrintVersion, "version", false, "--version 打印程序构建版本")

flag.Parse()
Expand Down Expand Up @@ -91,8 +86,15 @@ func initLogger() {
level := getLevel()
encoding := configs.LogEncoding
// 保留两个变量,但设置成同一个文件
stdout := configs.DataPath + "/log/stdout.log"
stderr := configs.DataPath + "/log/stdout.log"
//stdout := configs.DataPath + "/log/stdout.log"
//stderr := configs.DataPath + "/log/stderr.log"

stdout := &lumberjack.Logger{
Filename: configs.DataPath + "/log/stdout.log",
MaxSize: 10, // 每个日志文件的最大大小,单位为MB
MaxBackups: 10, // 保留的旧日志文件的最大数量
MaxAge: 60, // 保留的旧日志文件的最大天数
}

encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
Expand All @@ -114,11 +116,9 @@ func initLogger() {
Encoding: encoding, // 输出格式 console 或 json
EncoderConfig: encoderConfig, // 编码器配置
//InitialFields: map[string]interface{}{"serviceName": "gogs-backup"}, // 初始化字段,如:添加一个服务器名称
OutputPaths: []string{"stdout", stdout}, // 输出到指定文件 stdout(标准输出,正常颜色) stderr(错误输出,红色)
ErrorOutputPaths: []string{"stderr", stderr},
OutputPaths: []string{"stdout", stdout.Filename}, // 输出到指定文件 stdout(标准输出,正常颜色) stderr(错误输出,红色)
ErrorOutputPaths: []string{"stderr", stdout.Filename},
}
tools.Create(stdout)
tools.Create(stderr)
logger, err := config.Build()
if err != nil {
panic(fmt.Sprintf("logger 初始化失败: %v", err))
Expand Down Expand Up @@ -152,15 +152,6 @@ func getLevel() zapcore.Level {
return level
}

// 初始化 浏览器
func initBrowser() {
bow := surf.NewBrowser()
bow.SetUserAgent(agent.Chrome())
bow.HistoryJar().SetMax(1)
bow.SetTimeout(time.Duration(configs.TimeOut) * time.Second)
configs.Browser = bow
}

// 初始化 定时任务
func initCron() {
cronApp := cron.New(cron.WithSeconds()) //精确到秒
Expand All @@ -178,10 +169,9 @@ func initCron() {
// 定时获取流量信息
func cronSummary() {
t := carbon.Now()
detailRequest := tools.GetFlowDetail(true)
balance := tools.GetBalance(true)
flowPackage := tools.GetFlowPackage(true)
configs.Summary = tools.ToSummary2(detailRequest, flowPackage, balance, configs.Username, t)
qryImportantData := tools.GetQryImportantData(configs.Username, configs.Password)
//userFluxPackage := tools.GetUserFluxPackage(configs.Username, configs.Password)
configs.Summary = tools.ToSummary(qryImportantData, configs.Username, t)
}

// 初始化访问接口
Expand All @@ -193,8 +183,9 @@ func initIris() {
irisApp.Handle(iris.MethodGet, "/show/flowPackage", flowPackage)
if configs.Dev {
irisApp.Handle(iris.MethodGet, "/refresh", refresh)
irisApp.Handle(iris.MethodGet, "/image/{filename:string}", image)
irisApp.Handle(iris.MethodGet, "/loginLog", loginLog)

irisApp.Handle(iris.MethodGet, "/show/qryImportantData", qryImportantData)
irisApp.Handle(iris.MethodGet, "/show/userFluxPackage", userFluxPackage)
}
err := irisApp.Run(iris.Addr(":" + configs.Prot))
if err != nil {
Expand All @@ -220,42 +211,52 @@ func flow(ctx iris.Context) {
flowLastTime = carbon.Now()

t := carbon.Now()
detailRequest := tools.GetFlowDetail(false)
if detailRequest.Result != 0 {
go cronSummary()
} else {
balance := tools.GetBalance(true)
flowPackage := tools.GetFlowPackage(true)
configs.Summary = tools.ToSummary2(detailRequest, flowPackage, balance, configs.Username, t)
}

qryImportantData := tools.GetQryImportantData(configs.Username, configs.Password)
configs.Summary = tools.ToSummary(qryImportantData, configs.Username, t)

summary := desensitization(configs.Summary)
ctx.JSON(iris.Map{"code": 200, "data": summary})
}

var packageDetailVisitLastTime carbon.Carbon
var packageDetailDetailRequest *models.DetailRequest

func packageDetail(ctx iris.Context) {
if carbon.Now().Lt(packageDetailVisitLastTime.AddSeconds(configs.IntervalsTime)) {
ctx.JSON(&packageDetailDetailRequest)
ctx.JSON(&models.DetailRequest{
Result: 410,
ParaFieldResult: "接口已失效",
})
}

func flowPackage(ctx iris.Context) {
ctx.JSON(&models.FlowPackage{
Result: 410,
Msg: "接口已失效",
})
}

var qryImportantVisitLastTime carbon.Carbon
var qryImportantDetailRequest *models.Result[models.ImportantData]

func qryImportantData(ctx iris.Context) {
if carbon.Now().Lt(qryImportantVisitLastTime.AddSeconds(configs.IntervalsTime)) {
ctx.JSON(&qryImportantDetailRequest)
return
}
packageDetailDetailRequest = tools.GetFlowDetail(false)
packageDetailVisitLastTime = carbon.Now()
ctx.JSON(&packageDetailDetailRequest)
qryImportantDetailRequest = tools.GetQryImportantData(configs.Username, configs.Password)
qryImportantVisitLastTime = carbon.Now()
ctx.JSON(&qryImportantDetailRequest)
}

var flowPackageVisitLastTime carbon.Carbon
var flowPackageDetailRequest *models.FlowPackage
var userFluxPackageVisitLastTime carbon.Carbon
var userFluxPackageDetailRequest *models.Result[models.UserFluxPackageData]

func flowPackage(ctx iris.Context) {
if carbon.Now().Lt(flowPackageVisitLastTime.AddSeconds(configs.IntervalsTime)) {
ctx.JSON(&flowPackageDetailRequest)
func userFluxPackage(ctx iris.Context) {
if carbon.Now().Lt(userFluxPackageVisitLastTime.AddSeconds(configs.IntervalsTime)) {
ctx.JSON(&userFluxPackageDetailRequest)
return
}
flowPackageDetailRequest = tools.GetFlowPackage(false)
flowPackageVisitLastTime = carbon.Now()
ctx.JSON(&flowPackageDetailRequest)
userFluxPackageDetailRequest = tools.GetUserFluxPackage(configs.Username, configs.Password)
userFluxPackageVisitLastTime = carbon.Now()
ctx.JSON(&userFluxPackageDetailRequest)
}

func desensitization(summary models.Summary) models.Summary {
Expand All @@ -276,29 +277,3 @@ func refresh(ctx iris.Context) {
go cronSummary()
ctx.JSON(iris.Map{"code": 200})
}

func image(ctx iris.Context) {
filename := ctx.Params().GetStringDefault("filename", "01.png")
ctx.ServeFile(configs.DataPath+"/login/"+filename, false)
}

func loginLog(ctx iris.Context) {
ctx.HTML(`
<html>
<div style="text-align: center;">
<br/>
<h3>登录图片1</h3>
<br/>
<image style="max-width:500px" src="/image/01.png"></image>
<br/>
<h3>登录图片2</h3>
<br/>
<image style="max-width:500px" src="/image/02.png"></image>
<br/>
<h3>登录图片3</h3>
<br/>
<image style="max-width:500px" src="/image/03.png"></image>
</div>
</html>
`)
}
Loading

0 comments on commit 8bb771f

Please sign in to comment.