refactor: introduce service layer and migrate gRPC handlers to use it
All checks were successful
Build / build (push) Successful in 2m17s

Extract business logic from gRPC handlers into a dedicated service package.
Add context support throughout for cancellation and timeouts. Move models to
their own package, remove hardcoded credentials from config, and simplify
parsers to only handle HTML parsing.
This commit is contained in:
lafay
2026-06-16 18:33:13 +08:00
parent ae9297c11c
commit ab37aeb2fc
26 changed files with 603 additions and 683 deletions

75
client/encoding.go Normal file
View File

@@ -0,0 +1,75 @@
package client
import (
"bytes"
"io"
"net/http"
"strings"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)
// DecodeResponseBody 读取 HTTP 响应体并按 Content-Type 进行字符集解码(处理 GBK
func DecodeResponseBody(resp *http.Response) (string, error) {
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return decodeBody(body, resp.Header.Get("Content-Type")), nil
}
func decodeBody(body []byte, contentType string) string {
lowerCT := strings.ToLower(contentType)
if strings.Contains(lowerCT, "charset=gb") || strings.Contains(lowerCT, "charset=gb2312") || strings.Contains(lowerCT, "charset=gbk") {
decoded, err := decodeGBK(body)
if err == nil {
return decoded
}
}
if isValidUTF8(body) {
return string(body)
}
decoded, err := decodeGBK(body)
if err == nil {
return decoded
}
return string(body)
}
func decodeGBK(data []byte) (string, error) {
reader := transform.NewReader(bytes.NewReader(data), simplifiedchinese.GBK.NewDecoder())
decoded, err := io.ReadAll(reader)
if err != nil {
return "", err
}
return string(decoded), nil
}
func isValidUTF8(data []byte) bool {
for i := 0; i < len(data); i++ {
if data[i]&0x80 != 0 {
if data[i]&0xE0 == 0xC0 {
if i+1 >= len(data) || data[i+1]&0xC0 != 0x80 {
return false
}
i++
} else if data[i]&0xF0 == 0xE0 {
if i+2 >= len(data) || data[i+1]&0xC0 != 0x80 || data[i+2]&0xC0 != 0x80 {
return false
}
i += 2
} else if data[i]&0xF8 == 0xF0 {
if i+3 >= len(data) || data[i+1]&0xC0 != 0x80 || data[i+2]&0xC0 != 0x80 || data[i+3]&0xC0 != 0x80 {
return false
}
i += 3
} else {
return false
}
}
}
return true
}

47
client/fetch.go Normal file
View File

@@ -0,0 +1,47 @@
package client
import (
"context"
"fmt"
"io"
"net/http"
"schedule_converter/config"
)
// FetchHTML 发送 HTTP 请求并返回解码后的 HTML 字符串(处理 GBK 字符集)。
// method 为 "GET" 或 "POST"path 拼接到 config.BaseURL 之后body 为请求体GET 传 nil
// referer 为可选的 Referer 路径(拼接到 BaseURL 之后),不需要时传 ""。
func FetchHTML(ctx context.Context, httpClient *http.Client, method, path string, body io.Reader, contentType, referer string) (string, error) {
resp, err := doRequest(ctx, httpClient, method, path, body, contentType, referer)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("请求 %s 失败, 状态码: %d", path, resp.StatusCode)
}
return DecodeResponseBody(resp)
}
// doRequest 构造并执行请求,设置 header可选 Referer透传 ctx。
func doRequest(ctx context.Context, httpClient *http.Client, method, path string, body io.Reader, contentType, referer string) (*http.Response, error) {
url := config.Get().BaseURL + path
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
if contentType != "" {
req.Header = GetHeadersWithContentType(contentType)
} else {
req.Header = GetHeaders()
}
if referer != "" {
req.Header.Set("Referer", config.Get().BaseURL+referer)
}
return httpClient.Do(req)
}