fly 3 месяцев назад
Сommit
126aee7387
13 измененных файлов с 1156 добавлено и 0 удалено
  1. 49 0
      database/database.go
  2. 17 0
      go.mod
  3. 16 0
      go.sum
  4. 169 0
      handlers/parse_handler.go
  5. 104 0
      handlers/user_handler.go
  6. 51 0
      main.go
  7. 13 0
      models/parse.go
  8. 10 0
      models/user.go
  9. 33 0
      third/parse.go
  10. 105 0
      third/parse_test.go
  11. 215 0
      third/parser.go
  12. 211 0
      third/req.go
  13. 163 0
      third/req_test.go

+ 49 - 0
database/database.go

@@ -0,0 +1,49 @@
+package database
+
+import (
+	"log"
+	"os"
+
+	"git.familybaby.top/flight/csdn/models"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+)
+
+var DB *gorm.DB
+
+func InitDB() error {
+	dbPath := "device.db"
+	if os.Getenv("DB_PATH") != "" {
+		dbPath = os.Getenv("DB_PATH")
+	}
+
+	var err error
+	DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
+	if err != nil {
+		return err
+	}
+
+	// 自动迁移表结构
+	err = DB.AutoMigrate(
+		&models.User{},
+		&models.Parse{},
+	)
+	if err != nil {
+		return err
+	}
+
+	log.Println("Database initialized successfully")
+	return nil
+}
+
+func CloseDB() {
+	db, err := DB.DB()
+	if err != nil {
+		log.Println("Error getting database instance:", err)
+		return
+	}
+	err = db.Close()
+	if err != nil {
+		log.Println("Error closing database:", err)
+	}
+}

+ 17 - 0
go.mod

@@ -0,0 +1,17 @@
+module git.familybaby.top/flight/csdn
+
+go 1.21.10
+
+require (
+	git.familybaby.top/flight/utils v1.0.8
+	github.com/gorilla/mux v1.8.1
+	gorm.io/driver/sqlite v1.6.0
+	gorm.io/gorm v1.30.0
+)
+
+require (
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/mattn/go-sqlite3 v1.14.22 // indirect
+	golang.org/x/text v0.20.0 // indirect
+)

+ 16 - 0
go.sum

@@ -0,0 +1,16 @@
+git.familybaby.top/flight/utils v1.0.8 h1:KUp/3RM7+tZ22+W/x/dcPtyzFhoxjTW1bPwSwlF17tQ=
+git.familybaby.top/flight/utils v1.0.8/go.mod h1:fG+qm7ZlHufyUte64skPu3FbVg7DotIhbyH0GyV6QBU=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
+gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
+gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
+gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
+gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=

+ 169 - 0
handlers/parse_handler.go

@@ -0,0 +1,169 @@
+package handlers
+
+import (
+	"encoding/json"
+	"git.familybaby.top/flight/csdn/third"
+	"net/http"
+	"strconv"
+	"time"
+
+	"git.familybaby.top/flight/csdn/database"
+	"git.familybaby.top/flight/csdn/models"
+	"github.com/gorilla/mux"
+)
+
+var (
+	parser = third.NewParser("tzlbndf9pi7ebzky", "https://unlockdoc.smain.cn/api/api")
+)
+
+type ParseInfo struct {
+	Url    string `json:"url"`
+	UserId string `json:"userId"`
+}
+
+func CheckUrlInvalid(w http.ResponseWriter, r *http.Request) {
+	var info ParseInfo
+	err := json.NewDecoder(r.Body).Decode(&info)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	price, err := parser.GetTaskPrice(info.Url)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusCreated)
+	json.NewEncoder(w).Encode(price)
+}
+
+func CreateParse(w http.ResponseWriter, r *http.Request) {
+	var info ParseInfo
+	err := json.NewDecoder(r.Body).Decode(&info)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	price, err := parser.PurchasePaperPlusProxy(info.Url)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	records, err := parser.GetRecords()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	var Parse models.Parse
+	var hasValue = false
+	for _, record := range records {
+		if record.Source == info.Url && record.HtmlUrl == price {
+			Parse.Title = record.Title
+			ct, err := time.Parse("2006-01-02 15:04:05", record.Date)
+			if err == nil {
+				Parse.CreatedAt = ct
+			}
+			Parse.HtmlUrl = record.HtmlUrl
+			Parse.OnlineUrl = record.OnlineHtmlUrl
+			Parse.SourceUrl = record.Source
+
+			hasValue = true
+			break
+		}
+	}
+
+	if !hasValue {
+		http.Error(w, "解析失败", http.StatusInternalServerError)
+		return
+	}
+
+	result := database.DB.Create(&Parse)
+	if result.Error != nil {
+		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusCreated)
+	json.NewEncoder(w).Encode(Parse)
+}
+
+func GetParses(w http.ResponseWriter, r *http.Request) {
+	var Parses []models.Parse
+	result := database.DB.Find(&Parses)
+	if result.Error != nil {
+		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(Parses)
+}
+
+func GetParse(w http.ResponseWriter, r *http.Request) {
+	params := mux.Vars(r)
+	id, err := strconv.Atoi(params["id"])
+	if err != nil {
+		http.Error(w, "Invalid ID", http.StatusBadRequest)
+		return
+	}
+
+	var Parse models.Parse
+	result := database.DB.First(&Parse, id)
+	if result.Error != nil {
+		http.Error(w, result.Error.Error(), http.StatusNotFound)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(Parse)
+}
+
+func UpdateParse(w http.ResponseWriter, r *http.Request) {
+	params := mux.Vars(r)
+	id, err := strconv.Atoi(params["id"])
+	if err != nil {
+		http.Error(w, "Invalid ID", http.StatusBadRequest)
+		return
+	}
+
+	var Parse models.Parse
+	err = json.NewDecoder(r.Body).Decode(&Parse)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	result := database.DB.Model(&models.Parse{}).Where("id = ?", id).Updates(Parse)
+	if result.Error != nil {
+		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(Parse)
+}
+
+func DeleteParse(w http.ResponseWriter, r *http.Request) {
+	params := mux.Vars(r)
+	id, err := strconv.Atoi(params["id"])
+	if err != nil {
+		http.Error(w, "Invalid ID", http.StatusBadRequest)
+		return
+	}
+
+	result := database.DB.Delete(&models.Parse{}, id)
+	if result.Error != nil {
+		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.WriteHeader(http.StatusNoContent)
+}

+ 104 - 0
handlers/user_handler.go

@@ -0,0 +1,104 @@
+package handlers
+
+import (
+	"encoding/json"
+	"net/http"
+	"strconv"
+
+	"git.familybaby.top/flight/csdn/database"
+	"git.familybaby.top/flight/csdn/models"
+	"github.com/gorilla/mux"
+)
+
+func CreateUser(w http.ResponseWriter, r *http.Request) {
+	var User models.User
+	err := json.NewDecoder(r.Body).Decode(&User)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	result := database.DB.Create(&User)
+	if result.Error != nil {
+		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusCreated)
+	json.NewEncoder(w).Encode(User)
+}
+
+func GetUsers(w http.ResponseWriter, r *http.Request) {
+	var Users []models.User
+	result := database.DB.Find(&Users)
+	if result.Error != nil {
+		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(Users)
+}
+
+func GetUser(w http.ResponseWriter, r *http.Request) {
+	params := mux.Vars(r)
+	id, err := strconv.Atoi(params["id"])
+	if err != nil {
+		http.Error(w, "Invalid ID", http.StatusBadRequest)
+		return
+	}
+
+	var User models.User
+	result := database.DB.First(&User, id)
+	if result.Error != nil {
+		http.Error(w, result.Error.Error(), http.StatusNotFound)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(User)
+}
+
+func UpdateUser(w http.ResponseWriter, r *http.Request) {
+	params := mux.Vars(r)
+	id, err := strconv.Atoi(params["id"])
+	if err != nil {
+		http.Error(w, "Invalid ID", http.StatusBadRequest)
+		return
+	}
+
+	var User models.User
+	err = json.NewDecoder(r.Body).Decode(&User)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	result := database.DB.Model(&models.User{}).Where("id = ?", id).Updates(User)
+	if result.Error != nil {
+		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(User)
+}
+
+func DeleteUser(w http.ResponseWriter, r *http.Request) {
+	params := mux.Vars(r)
+	id, err := strconv.Atoi(params["id"])
+	if err != nil {
+		http.Error(w, "Invalid ID", http.StatusBadRequest)
+		return
+	}
+
+	result := database.DB.Delete(&models.User{}, id)
+	if result.Error != nil {
+		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.WriteHeader(http.StatusNoContent)
+}

+ 51 - 0
main.go

@@ -0,0 +1,51 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+
+	"git.familybaby.top/flight/csdn/database"
+	"git.familybaby.top/flight/csdn/handlers"
+	"github.com/gorilla/mux"
+)
+
+func main() {
+	// 初始化数据库
+	err := database.InitDB()
+	if err != nil {
+		log.Fatal("Failed to initialize database:", err)
+	}
+	defer database.CloseDB()
+
+	// 创建路由器
+	r := mux.NewRouter()
+
+	// 解析路由
+	r.HandleFunc("/parse", handlers.CreateParse).Methods("POST")
+	r.HandleFunc("/parse", handlers.GetParses).Methods("GET")
+	r.HandleFunc("/parse/check", handlers.CheckUrlInvalid).Methods("GET")
+	r.HandleFunc("/parse/{id}", handlers.GetParse).Methods("GET")
+	r.HandleFunc("/parse/{id}", handlers.UpdateParse).Methods("PUT")
+	r.HandleFunc("/parse/{id}", handlers.DeleteParse).Methods("DELETE")
+
+	// 用户路由
+	r.HandleFunc("/user", handlers.CreateUser).Methods("POST")
+	r.HandleFunc("/user", handlers.GetUsers).Methods("GET")
+	r.HandleFunc("/user/{id}", handlers.GetUser).Methods("GET")
+	r.HandleFunc("/user/{id}", handlers.UpdateUser).Methods("PUT")
+	r.HandleFunc("/user/{id}", handlers.DeleteUser).Methods("DELETE")
+
+	// 静态文件服务 (用于Vue前端)
+	fs := http.FileServer(http.Dir("./static"))
+	r.PathPrefix("/").Handler(fs)
+
+	// 启动服务器
+	port := os.Getenv("PORT")
+	if port == "" {
+		port = "8080"
+	}
+	fmt.Println("Server starting on port", port)
+	log.Fatal(http.ListenAndServe(":"+port, r))
+}

+ 13 - 0
models/parse.go

@@ -0,0 +1,13 @@
+package models
+
+import "gorm.io/gorm"
+
+type Parse struct {
+	gorm.Model
+	Title     string `json:"title"`
+	HtmlUrl   string `json:"htmlUrl"`
+	OnlineUrl string `json:"onlineUrl"`
+	SourceUrl string `json:"sourceUrl"`
+	UserID    string `json:"userId"`
+	User      User   `gorm:"foreignKey:UserID"`
+}

+ 10 - 0
models/user.go

@@ -0,0 +1,10 @@
+package models
+
+import "gorm.io/gorm"
+
+type User struct {
+	gorm.Model
+	Username    string `json:"username"`
+	Password    string `json:"password"`
+	UniqueIndex string `json:"uniqueIndex"`
+}

+ 33 - 0
third/parse.go

@@ -0,0 +1,33 @@
+package third
+
+import (
+	"encoding/base64"
+	"git.familybaby.top/flight/utils/algorithm/encryption"
+)
+
+var (
+	key = "MTIzNDU2Nzg5MEFC"
+	iv  = "QUJDRURGMDk4NzY1"
+	aes = encryption.NewAesCBCWithPadding([]byte(key), []byte(iv))
+)
+
+func decrypt(in []byte) (string, error) {
+	ciphertext, err := base64.StdEncoding.DecodeString(string(in))
+	if err != nil {
+		return "", err
+	}
+
+	ciphertext, err = aes.Decrypt(ciphertext)
+
+	return string(ciphertext), err
+
+}
+
+func encrypt(in []byte) (string, error) {
+	r, err := aes.Encrypt(in)
+	if err != nil {
+		return "", err
+	}
+
+	return base64.StdEncoding.EncodeToString(r), nil
+}

+ 105 - 0
third/parse_test.go

@@ -0,0 +1,105 @@
+package third
+
+import "testing"
+
+func Test_decrypt(t *testing.T) {
+	type args struct {
+		in []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		wantErr bool
+	}{
+		// TODO: Add test cases.
+		{
+			name:    "https://unlockdoc.smain.cn/api/api/user/getUserInfoPlus",
+			args:    args{in: []byte("tjagpyNaMSjRxlfwxYMLsg==")},
+			want:    "{}",
+			wantErr: false,
+		},
+		{
+			name:    "https://unlockdoc.smain.cn/api/api/user/getUserInfoPlus",
+			args:    args{in: []byte("aJboPrg3FKs91jg4BmmKc4CDeIDyrNQqhanW1uVXQCo=")},
+			want:    "{\"code\":200,\"message\":\"成功\"}",
+			wantErr: false,
+		},
+		{
+			name:    "https://unlockdoc.smain.cn/api/api/user/getRecords",
+			args:    args{in: []byte("5fczz2FJXKZcx59LiJXI9rnuMH+vkb8r5MOq4k5jcvgwBbyVTuITC1AKnurpIUudDERL+qIU8E/hAeUXU18sjg==")},
+			want:    "{\"userToken\":\"df9pi7ebzky\",\"email\":\"df9pi7ebzky\"}",
+			wantErr: false,
+		},
+		{
+			name:    "https://unlockdoc.smain.cn/api/api/user/getRecords",
+			args:    args{in: []byte("aJboPrg3FKs91jg4BmmKc+5vw5ycrPr/pBR9r8v6dhbnoea471YYVelrqVTfl5pCI7GLEBzV2UMF4Xo3hwKfBpC+ioEsqJcbqawgV6LhGoZ/tcX94VzKlld2fi+UJEmJCUSIochS2fq57whI7ZQ6q4w610atfgGqQ6XFn/Mjxn77Za+LszFtHq0XxGukgragippUywJHGjyoILvK8ggWtqUNpzTwCRUNvVx4r4HZZNnYybYGy7o7FcZJX1f+DuvN6QMIOK2QgaIjXlnKLClI12BOnrI5+7j3yfVbHT7dmYvfOVgnlbr+gdZBMiLmP6mtu0gC0piJXWZyJa3ejy8ulbk1Aw29ErqtXVN5CUCAbPW2ICmPcVBtzYcZ4oTGsuZxYU4e4xjYSgeNUovE6WbGV5ptQPbt9AJ9hz03Uzv3U0n6QnKZUvjVhDD6p+M23s2x/l+jyL2T6oCcIdmtQsC0XTT+l8K8Jy6eMxZytPtsuhpJo1eTc69nTz2VD24YHY6udyjycUft2dG2S0q2Hf8FM1wToGREbPVmhnOz+HYbVYyd4W6Hu8DFVToBi+tWcJWzKSOj582CFVkUa3BfnUmepTxfkr//6KqTklz3dpyClPA/+imNuAS6/NE5gs4+HMNF9iPiHriQMD9MO+PVYsmVU7nxt0iJwzDpK3xn+kvSpNV4+bQiROz3OYGh/BlO2443CZsss9svhsbtcOOIQfpUhlx87LDwUN8vRGbJdleAWiIo03LeQTxTWhyJvsrK350WiyhQPDtG15x+C8Xo8yFrK34m/OqhB5H/8y++Uy9qWFyxeoJ3R/dI2DnQ6m1r0TwLP50qzvkCjoYE+jx69hXN7hkvSo8oG91eOiR464rS3H8Y47fES/kl6G6tnd/81xrCheVUNyXyBBipSZapZhNxnhaFWWvbvMO/9/5TA9WF3LVSagxASo3GvjFdGyNvmqFUkRbsOXOn3jEg589XsPvYi6Tel6yyVT1qlYoAgHFQeGkvcMANNCf25E4675bXZSEnk5rtaLgV6yY6X4fPduHthXwQ2qggvYfY7dio1zO829eh0CrgTFuNv5eNBJpmT1luzwMcWrkBFyOnvWVZ8q+dBHM0k7NFlrtNURr6ZbvEVquGmX3xNnD0uLxXCuaBB+D0wgQBP2JKE/zTV3Mo+phxL9WNNPq3qwvus4nJuQFYEz+klOXFLfOfPGcJHGWohTgRZbCeDuabnNa5h1HyBiIXPc2ELqQF9IirLxSM27jDnboEJrGLO2/ne7WVh5yvixAnIOC4yyf8YRpS28cw3InbOC6wsJ2Jkc6dyAFj4z/KbR4zmBnHhNFmZRKHmxtureVHv4QBVGS5TR2+lFvu4lG9IgzaCCIBT+R5o78YNBy96x3NP44GDiFIG7XqT3wL++VHEFvtWr1wlv2vYEe4H+/TKHgnWeXCWTZaL8OGD0IUsATaOVJi0Qh2WhfxHDgp/rAfmEOHSm2nrplfQiZrnlRE3e4lh0yEr0lCwBiuDSjuRyE=")},
+			want:    "{\"code\":200,\"message\":\"成功\",\"data\":[{\"id\":\"1930160779866267648\",\"email\":\"df9pi7ebzky\",\"date\":\"2025-06-04 15:12:45\",\"markdown_url\":\"https://mark.cuckooing.cn/mdstx/2025-04-05/SquidV6.8编译安装升级记录.md\",\"html_url\":\"https://mark.cuckooing.cn/mdstx/2025-04-05/SquidV6.8编译安装升级记录.html\",\"online_html_url\":\"https://unlockdoc.smain.cn/online/2025-06-04/9fae92c9a32218e0a3cb9587c60de2ad.html\",\"pdf_url\":null,\"source\":\"https://blog.csdn.net/2301_76390982/article/details/136744443\",\"title\":\"SquidV6.8编译安装升级记录\",\"isFail\":null},{\"id\":\"1930133868200976384\",\"email\":\"df9pi7ebzky\",\"date\":\"2025-06-04 13:25:48\",\"markdown_url\":\"https://unlockdoc.smain.cn/mds/2025-06-04/Golang中通过cgo调用C++的动态库的功能封装.md\",\"html_url\":\"https://unlockdoc.smain.cn/mds/2025-06-04/Golang中通过cgo调用C++的动态库的功能封装.html\",\"online_html_url\":\"https://unlockdoc.smain.cn/online/2025-06-04/e32d5340604be42eefbf7d0a1f36c10e.html\",\"pdf_url\":null,\"source\":\"https://blog.csdn.net/JineD/article/details/130735088\",\"title\":\"Golang中通过cgo调用C++的动态库的功能封装\",\"isFail\":null}]}",
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := decrypt(tt.args.in)
+			//t.Logf("name:%s, got: %s,err: %s", tt.name, got, err)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("decrypt() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("decrypt() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_encrypt(t *testing.T) {
+	type args struct {
+		in []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		wantErr bool
+	}{
+		// TODO: Add test cases.
+		{
+			name:    "t1",
+			args:    args{in: []byte("{}")},
+			want:    "tjagpyNaMSjRxlfwxYMLsg==",
+			wantErr: false,
+		},
+		{
+			name:    "https://unlockdoc.smain.cn/api/api/user/getUserInfoPlus",
+			args:    args{in: []byte("{\"code\":200,\"message\":\"成功\"}")},
+			want:    "aJboPrg3FKs91jg4BmmKc4CDeIDyrNQqhanW1uVXQCo=",
+			wantErr: false,
+		},
+		{
+			name:    "https://unlockdoc.smain.cn/api/api/user/getRecords",
+			args:    args{in: []byte("{\"userToken\":\"df9pi7ebzky\",\"email\":\"df9pi7ebzky\"}")},
+			want:    "5fczz2FJXKZcx59LiJXI9rnuMH+vkb8r5MOq4k5jcvgwBbyVTuITC1AKnurpIUudDERL+qIU8E/hAeUXU18sjg==",
+			wantErr: false,
+		},
+		{
+			name:    "https://unlockdoc.smain.cn/api/api/user/getRecords",
+			args:    args{in: []byte("{\"code\":200,\"message\":\"成功\",\"data\":[{\"id\":\"1930160779866267648\",\"email\":\"df9pi7ebzky\",\"date\":\"2025-06-04 15:12:45\",\"markdown_url\":\"https://mark.cuckooing.cn/mdstx/2025-04-05/SquidV6.8编译安装升级记录.md\",\"html_url\":\"https://mark.cuckooing.cn/mdstx/2025-04-05/SquidV6.8编译安装升级记录.html\",\"online_html_url\":\"https://unlockdoc.smain.cn/online/2025-06-04/9fae92c9a32218e0a3cb9587c60de2ad.html\",\"pdf_url\":null,\"source\":\"https://blog.csdn.net/2301_76390982/article/details/136744443\",\"title\":\"SquidV6.8编译安装升级记录\",\"isFail\":null},{\"id\":\"1930133868200976384\",\"email\":\"df9pi7ebzky\",\"date\":\"2025-06-04 13:25:48\",\"markdown_url\":\"https://unlockdoc.smain.cn/mds/2025-06-04/Golang中通过cgo调用C++的动态库的功能封装.md\",\"html_url\":\"https://unlockdoc.smain.cn/mds/2025-06-04/Golang中通过cgo调用C++的动态库的功能封装.html\",\"online_html_url\":\"https://unlockdoc.smain.cn/online/2025-06-04/e32d5340604be42eefbf7d0a1f36c10e.html\",\"pdf_url\":null,\"source\":\"https://blog.csdn.net/JineD/article/details/130735088\",\"title\":\"Golang中通过cgo调用C++的动态库的功能封装\",\"isFail\":null}]}")},
+			want:    "aJboPrg3FKs91jg4BmmKc+5vw5ycrPr/pBR9r8v6dhbnoea471YYVelrqVTfl5pCI7GLEBzV2UMF4Xo3hwKfBpC+ioEsqJcbqawgV6LhGoZ/tcX94VzKlld2fi+UJEmJCUSIochS2fq57whI7ZQ6q4w610atfgGqQ6XFn/Mjxn77Za+LszFtHq0XxGukgragippUywJHGjyoILvK8ggWtqUNpzTwCRUNvVx4r4HZZNnYybYGy7o7FcZJX1f+DuvN6QMIOK2QgaIjXlnKLClI12BOnrI5+7j3yfVbHT7dmYvfOVgnlbr+gdZBMiLmP6mtu0gC0piJXWZyJa3ejy8ulbk1Aw29ErqtXVN5CUCAbPW2ICmPcVBtzYcZ4oTGsuZxYU4e4xjYSgeNUovE6WbGV5ptQPbt9AJ9hz03Uzv3U0n6QnKZUvjVhDD6p+M23s2x/l+jyL2T6oCcIdmtQsC0XTT+l8K8Jy6eMxZytPtsuhpJo1eTc69nTz2VD24YHY6udyjycUft2dG2S0q2Hf8FM1wToGREbPVmhnOz+HYbVYyd4W6Hu8DFVToBi+tWcJWzKSOj582CFVkUa3BfnUmepTxfkr//6KqTklz3dpyClPA/+imNuAS6/NE5gs4+HMNF9iPiHriQMD9MO+PVYsmVU7nxt0iJwzDpK3xn+kvSpNV4+bQiROz3OYGh/BlO2443CZsss9svhsbtcOOIQfpUhlx87LDwUN8vRGbJdleAWiIo03LeQTxTWhyJvsrK350WiyhQPDtG15x+C8Xo8yFrK34m/OqhB5H/8y++Uy9qWFyxeoJ3R/dI2DnQ6m1r0TwLP50qzvkCjoYE+jx69hXN7hkvSo8oG91eOiR464rS3H8Y47fES/kl6G6tnd/81xrCheVUNyXyBBipSZapZhNxnhaFWWvbvMO/9/5TA9WF3LVSagxASo3GvjFdGyNvmqFUkRbsOXOn3jEg589XsPvYi6Tel6yyVT1qlYoAgHFQeGkvcMANNCf25E4675bXZSEnk5rtaLgV6yY6X4fPduHthXwQ2qggvYfY7dio1zO829eh0CrgTFuNv5eNBJpmT1luzwMcWrkBFyOnvWVZ8q+dBHM0k7NFlrtNURr6ZbvEVquGmX3xNnD0uLxXCuaBB+D0wgQBP2JKE/zTV3Mo+phxL9WNNPq3qwvus4nJuQFYEz+klOXFLfOfPGcJHGWohTgRZbCeDuabnNa5h1HyBiIXPc2ELqQF9IirLxSM27jDnboEJrGLO2/ne7WVh5yvixAnIOC4yyf8YRpS28cw3InbOC6wsJ2Jkc6dyAFj4z/KbR4zmBnHhNFmZRKHmxtureVHv4QBVGS5TR2+lFvu4lG9IgzaCCIBT+R5o78YNBy96x3NP44GDiFIG7XqT3wL++VHEFvtWr1wlv2vYEe4H+/TKHgnWeXCWTZaL8OGD0IUsATaOVJi0Qh2WhfxHDgp/rAfmEOHSm2nrplfQiZrnlRE3e4lh0yEr0lCwBiuDSjuRyE=",
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := encrypt(tt.args.in)
+			//t.Logf("name:%s, got: %s,err: %s", tt.name, got, err)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("encrypt() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("encrypt() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 215 - 0
third/parser.go

@@ -0,0 +1,215 @@
+package third
+
+import (
+	"encoding/json"
+	"errors"
+)
+
+type Parser struct {
+	card    string
+	baseUrl string
+}
+
+type ParserRespond struct {
+	Code    int         `json:"code"`
+	Message string      `json:"message"`
+	Data    interface{} `json:"data"`
+}
+
+func uPost(req interface{}, url string) (result ParserRespond, err error) {
+	t, _ := json.Marshal(req)
+	res, err := post(url, t)
+
+	if err != nil {
+		return
+	}
+
+	err = json.Unmarshal(res, &result)
+
+	if err != nil {
+		return
+	}
+
+	if result.Code != 200 {
+		return result, errors.New(result.Message)
+	}
+
+	return
+}
+
+/*
+功能: 新建一个解析器
+
+输入:
+
+	card: 当前用户卡密
+	baseUrl: 服务器基础路径
+
+返回:
+
+	respond: 解析记录切片
+	error:  解析报错
+*/
+func NewParser(card string, baseUrl string) *Parser {
+	return &Parser{card, baseUrl}
+}
+
+/*
+功能: 查看当前接口是否正常, 貌似没啥意义
+
+输入:
+
+	无
+
+返回:
+
+	respond: 正常/异常
+	error:  解析报错
+*/
+func (p *Parser) GetUserInfo() (bool, error) {
+	url := p.baseUrl + VipStatusUrl
+	_, err := post(url, []byte("{}"))
+
+	if err != nil {
+		return false, err
+	}
+
+	return true, nil
+}
+
+/*
+功能: 查看当前用户解析记录
+
+输入:
+
+	无
+
+返回:
+
+	respond: 解析记录切片
+	error:  解析报错
+*/
+func (p *Parser) GetRecords() ([]GetRecordsRespondData, error) {
+	url := p.baseUrl + VipRecordsUrl
+
+	key := getUserKey(p.card)
+	req := GetRecordsRequestParam{
+		UserToken: key,
+		Email:     key,
+	}
+
+	result, err := uPost(req, url)
+	if err != nil {
+		return nil, err
+	}
+
+	r, ok := result.Data.([]GetRecordsRespondData)
+	if !ok {
+		return nil, errors.New("断言数据类型错误")
+	}
+
+	return r, nil
+}
+
+/*
+功能: 查看当前用户客户端的信息
+
+输入:
+
+	无
+
+返回:
+
+	respond: 客户端信息
+	error:  解析报错
+*/
+func (p *Parser) GetCountProxy() (*GetCountProxyRespondData, error) {
+	url := p.baseUrl + VipProxyStatusUrl
+
+	key := getUserKey(p.card)
+	req := GetCountProxyRequestParam{
+		Key: key,
+	}
+
+	result, err := uPost(req, url)
+	if err != nil {
+		return nil, err
+	}
+
+	r, ok := result.Data.(GetCountProxyRespondData)
+	if !ok {
+		return nil, errors.New("断言数据类型错误")
+	}
+
+	return &r, nil
+}
+
+/*
+功能: 查看当前文章是否支持解析/或者路径是否正确
+
+输入:
+
+	source: csdn 文章路径
+
+返回:
+
+	bool: 支持/不支持
+	error:  解析报错
+*/
+func (p *Parser) GetTaskPrice(source string) (bool, error) {
+	url := p.baseUrl + VipTaskPriceUrl
+
+	req := GetTaskPriceRequestParam{
+		Url: source,
+	}
+
+	result, err := uPost(req, url)
+	if err != nil {
+		return false, err
+	}
+
+	r, ok := result.Data.(int)
+	if !ok {
+		return false, errors.New("断言数据类型错误")
+	}
+
+	return r == 1, nil
+}
+
+/*
+功能: 解析当前文章
+
+输入:
+
+	source: csdn 文章路径
+
+返回:
+
+	result: 解析的文章路径, 可以直接下载查看的路径
+	error:  解析报错
+*/
+func (p *Parser) PurchasePaperPlusProxy(source string) (string, error) {
+	url := p.baseUrl + VipParseUrl
+
+	key := getUserKey(p.card)
+	req := PurchasePaperPlusProxyRequestParam{
+		CssUrl: "https://unlockdoc.smain.cn/assets/13062df20.css",
+		Other:  "1",
+		Email:  nil,
+		Url:    source,
+		UseNum: 1,
+		Key:    key,
+	}
+
+	result, err := uPost(req, url)
+	if err != nil {
+		return "", err
+	}
+
+	r, ok := result.Data.(string)
+	if !ok {
+		return "", errors.New("断言数据类型错误")
+	}
+
+	return r, nil
+}

+ 211 - 0
third/req.go

@@ -0,0 +1,211 @@
+package third
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"time"
+)
+
+var (
+	card              = "tzlbndf9pi7ebzky"                   //当前有效的卡密
+	baseUrl           = "https://unlockdoc.smain.cn/api/api" //服务器的基础url
+	VipStatusUrl      = "/user/getUserInfoPlus"              //接口是否正常
+	VipRecordsUrl     = "/user/getRecords"                   //获取当前用户的解析记录
+	VipProxyStatusUrl = "/user/getCountProxy"                //获取当前解析客户端的信息
+	VipTaskPriceUrl   = "/user/getTaskPrice"                 //查看文章是否可以解析
+	VipParseUrl       = "/user/purchasePaperPlusProxy"       //解析对应的文章
+)
+
+func getUserKey(card string) string {
+	return strings.TrimPrefix(card, "tzlbn")
+}
+
+func post(url string, body []byte) (result []byte, err error) {
+	client := &http.Client{Timeout: 30 * time.Second}
+
+	param, _ := encrypt(body)
+
+	resp, err := client.Post(url, "application/json", bytes.NewBuffer([]byte(param)))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	result, _ = io.ReadAll(resp.Body)
+
+	res, _ := decrypt(result)
+	fmt.Println("result: ", string(result), "\r\nres: ", res, "\nparam:", param, "\nreq:", string(body))
+
+	return []byte(res), nil
+}
+
+func GetUserInfoPlus() (bool, error) {
+	url := baseUrl + VipStatusUrl
+	_, err := post(url, []byte("{}"))
+
+	if err != nil {
+		return false, err
+	}
+
+	return true, nil
+}
+
+type GetRecordsRequestParam struct {
+	UserToken string `json:"userToken"`
+	Email     string `json:"email"`
+}
+
+type GetRecordsRespondData struct {
+	Id            string      `json:"id"`              //记录编号
+	Email         string      `json:"email"`           //email 实际为卡密
+	Date          string      `json:"date"`            //时间
+	MarkdownUrl   string      `json:"markdown_url"`    //解析后的md 文件路径
+	HtmlUrl       string      `json:"html_url"`        //解析后的 html 文件路径
+	OnlineHtmlUrl string      `json:"online_html_url"` //解析口在线观看路径
+	PdfUrl        interface{} `json:"pdf_url"`         //pdf 路径, 暂不支持
+	Source        string      `json:"source"`          //原始文章路径
+	Title         string      `json:"title"`           //标题
+	IsFail        interface{} `json:"isFail"`          //是否失败
+}
+type GetRecordsRespondParam struct {
+	Code    int                     `json:"code"`
+	Message string                  `json:"message"`
+	Data    []GetRecordsRespondData `json:"data"`
+}
+
+func GetRecords(card string) (*GetRecordsRespondParam, error) {
+	url := baseUrl + VipRecordsUrl
+
+	key := getUserKey(card)
+	req := GetRecordsRequestParam{
+		UserToken: key,
+		Email:     key,
+	}
+
+	t, _ := json.Marshal(req)
+	res, err := post(url, t)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var result GetRecordsRespondParam
+	err = json.Unmarshal(res, &result)
+
+	return &result, err
+}
+
+type GetCountProxyRequestParam struct {
+	Key string `json:"key"`
+}
+
+type GetCountProxyRespondData struct {
+	Key        string      `json:"key"`        //用户卡信息
+	WatchDate  string      `json:"watchDate"`  //有效期
+	IsExpire   int         `json:"isExpire"`   //是否过期
+	UpdateDate string      `json:"updateDate"` //更新时间
+	CreateDate string      `json:"createDate"` //创建时间
+	Ip         string      `json:"ip"`         //客户端ip
+	IsProxy    interface{} `json:"isProxy"`    //是否为代理用户
+	Email      interface{} `json:"email"`      //email
+}
+type GetCountProxyRespondParam struct {
+	Code    int                      `json:"code"`
+	Message string                   `json:"message"`
+	Data    GetCountProxyRespondData `json:"data"`
+}
+
+func GetCountProxy(card string) (*GetCountProxyRespondParam, error) {
+	url := baseUrl + VipProxyStatusUrl
+
+	key := getUserKey(card)
+	req := GetCountProxyRequestParam{
+		Key: key,
+	}
+
+	t, _ := json.Marshal(req)
+	res, err := post(url, t)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var result GetCountProxyRespondParam
+	err = json.Unmarshal(res, &result)
+
+	return &result, err
+}
+
+type GetTaskPriceRequestParam struct {
+	Url string `json:"url"`
+}
+type GeTaskPriceRespondParam struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+	Data    int    `json:"data"` //路径是否正确
+}
+
+func GetTaskPrice(source string) (*GeTaskPriceRespondParam, error) {
+	url := baseUrl + VipTaskPriceUrl
+
+	req := GetTaskPriceRequestParam{
+		Url: source,
+	}
+
+	t, _ := json.Marshal(req)
+	res, err := post(url, t)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var result GeTaskPriceRespondParam
+	err = json.Unmarshal(res, &result)
+
+	return &result, err
+}
+
+type PurchasePaperPlusProxyRequestParam struct {
+	CssUrl string      `json:"cssUrl"`
+	Other  string      `json:"other"`
+	Email  interface{} `json:"email"`
+	Url    string      `json:"url"`
+	UseNum int         `json:"useNum"`
+	Key    string      `json:"key"`
+}
+
+type PurchasePaperPlusProxyRespondParam struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+	ResUrl  string `json:"data"` //解析后的 可直接下载观看的路径
+}
+
+func PurchasePaperPlusProxy(card string, source string) (*PurchasePaperPlusProxyRespondParam, error) {
+	url := baseUrl + VipParseUrl
+
+	key := getUserKey(card)
+	req := PurchasePaperPlusProxyRequestParam{
+		CssUrl: "https://unlockdoc.smain.cn/assets/13062df20.css",
+		Other:  "1",
+		Email:  nil,
+		Url:    source,
+		UseNum: 1,
+		Key:    key,
+	}
+
+	t, _ := json.Marshal(req)
+	res, err := post(url, t)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var result PurchasePaperPlusProxyRespondParam
+	err = json.Unmarshal(res, &result)
+
+	return &result, err
+}

+ 163 - 0
third/req_test.go

@@ -0,0 +1,163 @@
+package third
+
+import (
+	"testing"
+)
+
+func TestGetUserInfoPlus(t *testing.T) {
+	type args struct {
+		key string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    bool
+		wantErr bool
+	}{
+		// TODO: Add test cases.
+		{
+			name:    "ok",
+			args:    args{""},
+			want:    true,
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := GetUserInfoPlus()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("GetParseStatus() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("GetParseStatus() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_GetRecords(t *testing.T) {
+	type args struct {
+		key string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    bool
+		wantErr bool
+	}{
+		// TODO: Add test cases.
+		{
+			name:    "ok",
+			args:    args{"tzlbndf9pi7ebzky"},
+			want:    true,
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := GetRecords(tt.args.key)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("getRecords() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got == nil {
+				t.Errorf("getRecords() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestPurchasePaperPlusProxy(t *testing.T) {
+	type args struct {
+		card   string
+		source string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *PurchasePaperPlusProxyRespondParam
+		wantErr bool
+	}{
+		// TODO: Add test cases.
+		{
+			name:    "ok",
+			args:    args{"tzlbndf9pi7ebzky", "https://blog.csdn.net/mopmgerg54mo/article/details/145314910"},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := PurchasePaperPlusProxy(tt.args.card, tt.args.source)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("PurchasePaperPlusProxy() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got.Code != 200 {
+				t.Errorf("PurchasePaperPlusProxy() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestGetTaskPrice(t *testing.T) {
+	type args struct {
+		source string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *GeTaskPriceRespondParam
+		wantErr bool
+	}{
+		// TODO: Add test cases.
+		{
+			name:    "ok",
+			args:    args{"https://blog.csdn.net/mopmgerg54mo/article/details/145314910"},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := GetTaskPrice(tt.args.source)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("GetTaskPrice() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got.Code != 200 {
+				t.Errorf("GetTaskPrice() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestGetCountProxy(t *testing.T) {
+	type args struct {
+		Key string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *GetCountProxyRespondParam
+		wantErr bool
+	}{
+		// TODO: Add test cases.
+		{
+			name:    "ok",
+			args:    args{"tzlbndf9pi7ebzky"},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := GetCountProxy(tt.args.Key)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("GetCountProxy() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got.Code != 200 {
+				t.Errorf("GetCountProxy() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}