2023-06-15 09:41:30 +08:00
|
|
|
|
package core
|
|
|
|
|
|
|
|
|
|
import (
|
2023-11-23 17:50:55 +08:00
|
|
|
|
"bytes"
|
2023-06-15 09:41:30 +08:00
|
|
|
|
"chatplus/core/types"
|
|
|
|
|
"chatplus/store/model"
|
|
|
|
|
"chatplus/utils"
|
|
|
|
|
"chatplus/utils/resp"
|
|
|
|
|
"context"
|
2023-09-05 11:47:03 +08:00
|
|
|
|
"fmt"
|
2023-06-15 09:41:30 +08:00
|
|
|
|
"github.com/gin-gonic/gin"
|
2023-09-05 11:47:03 +08:00
|
|
|
|
"github.com/go-redis/redis/v8"
|
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
2023-11-28 12:04:02 +08:00
|
|
|
|
"github.com/nfnt/resize"
|
2023-06-15 09:41:30 +08:00
|
|
|
|
"gorm.io/gorm"
|
2023-11-28 12:04:02 +08:00
|
|
|
|
"image"
|
|
|
|
|
"image/jpeg"
|
2023-06-15 09:41:30 +08:00
|
|
|
|
"io"
|
2023-11-28 12:04:02 +08:00
|
|
|
|
"log"
|
2023-06-15 09:41:30 +08:00
|
|
|
|
"net/http"
|
2023-11-28 12:04:02 +08:00
|
|
|
|
"os"
|
2023-06-15 09:41:30 +08:00
|
|
|
|
"runtime/debug"
|
|
|
|
|
"strings"
|
2023-09-05 11:47:03 +08:00
|
|
|
|
"time"
|
2023-06-15 09:41:30 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type AppServer struct {
|
2023-06-27 12:11:55 +08:00
|
|
|
|
Debug bool
|
2023-07-02 20:51:13 +08:00
|
|
|
|
Config *types.AppConfig
|
2023-06-15 09:41:30 +08:00
|
|
|
|
Engine *gin.Engine
|
2024-03-11 14:09:19 +08:00
|
|
|
|
ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message
|
2023-07-31 06:56:28 +08:00
|
|
|
|
|
2024-03-15 18:35:10 +08:00
|
|
|
|
SysConfig *types.SystemConfig // system config cache
|
2023-06-15 09:41:30 +08:00
|
|
|
|
|
|
|
|
|
// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次
|
|
|
|
|
// 防止第三方直接连接 socket 调用 OpenAI API
|
2023-08-17 14:20:16 +08:00
|
|
|
|
ChatSession *types.LMap[string, *types.ChatSession] //map[sessionId]UserId
|
2023-07-10 18:59:53 +08:00
|
|
|
|
ChatClients *types.LMap[string, *types.WsClient] // map[sessionId]Websocket 连接集合
|
2023-06-16 15:32:11 +08:00
|
|
|
|
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
2023-06-15 09:41:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-12-29 09:02:55 +08:00
|
|
|
|
func NewServer(appConfig *types.AppConfig) *AppServer {
|
2023-06-15 09:41:30 +08:00
|
|
|
|
gin.SetMode(gin.ReleaseMode)
|
|
|
|
|
gin.DefaultWriter = io.Discard
|
|
|
|
|
return &AppServer{
|
2023-06-27 12:11:55 +08:00
|
|
|
|
Debug: false,
|
2023-07-02 20:51:13 +08:00
|
|
|
|
Config: appConfig,
|
2023-06-15 09:41:30 +08:00
|
|
|
|
Engine: gin.Default(),
|
2024-03-11 14:09:19 +08:00
|
|
|
|
ChatContexts: types.NewLMap[string, []types.Message](),
|
2023-08-17 14:20:16 +08:00
|
|
|
|
ChatSession: types.NewLMap[string, *types.ChatSession](),
|
2023-06-16 15:32:11 +08:00
|
|
|
|
ChatClients: types.NewLMap[string, *types.WsClient](),
|
|
|
|
|
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
2023-06-15 09:41:30 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-05 11:47:03 +08:00
|
|
|
|
func (s *AppServer) Init(debug bool, client *redis.Client) {
|
2023-06-15 09:41:30 +08:00
|
|
|
|
if debug { // 调试模式允许跨域请求 API
|
2023-06-27 12:11:55 +08:00
|
|
|
|
s.Debug = debug
|
2023-06-15 09:41:30 +08:00
|
|
|
|
logger.Info("Enabled debug mode")
|
|
|
|
|
}
|
2023-07-31 06:56:28 +08:00
|
|
|
|
s.Engine.Use(corsMiddleware())
|
2023-11-28 12:04:02 +08:00
|
|
|
|
s.Engine.Use(staticResourceMiddleware())
|
2023-09-05 11:47:03 +08:00
|
|
|
|
s.Engine.Use(authorizeMiddleware(s, client))
|
2023-11-23 17:50:55 +08:00
|
|
|
|
s.Engine.Use(parameterHandlerMiddleware())
|
2023-06-15 09:41:30 +08:00
|
|
|
|
s.Engine.Use(errorHandler)
|
2023-06-26 18:18:45 +08:00
|
|
|
|
// 添加静态资源访问
|
2023-07-02 20:51:13 +08:00
|
|
|
|
s.Engine.Static("/static", s.Config.StaticDir)
|
2023-06-15 09:41:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *AppServer) Run(db *gorm.DB) error {
|
2023-07-31 06:56:28 +08:00
|
|
|
|
// load system configs
|
2023-07-31 08:13:20 +08:00
|
|
|
|
var sysConfig model.Config
|
2024-03-15 18:35:10 +08:00
|
|
|
|
res := db.Where("marker", "system").First(&sysConfig)
|
2023-07-31 06:56:28 +08:00
|
|
|
|
if res.Error != nil {
|
|
|
|
|
return res.Error
|
|
|
|
|
}
|
2024-03-15 18:35:10 +08:00
|
|
|
|
err := utils.JsonDecode(sysConfig.Config, &s.SysConfig)
|
2023-07-31 06:56:28 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2023-07-02 20:51:13 +08:00
|
|
|
|
logger.Infof("http://%s", s.Config.Listen)
|
|
|
|
|
return s.Engine.Run(s.Config.Listen)
|
2023-06-15 09:41:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 全局异常处理
|
|
|
|
|
func errorHandler(c *gin.Context) {
|
|
|
|
|
defer func() {
|
|
|
|
|
if r := recover(); r != nil {
|
2023-07-02 20:51:13 +08:00
|
|
|
|
logger.Errorf("Handler Panic: %v", r)
|
2023-06-15 09:41:30 +08:00
|
|
|
|
debug.PrintStack()
|
|
|
|
|
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
|
|
|
|
|
c.Abort()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
//加载完 defer recover,继续后续接口调用
|
|
|
|
|
c.Next()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 跨域中间件设置
|
|
|
|
|
func corsMiddleware() gin.HandlerFunc {
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
method := c.Request.Method
|
|
|
|
|
origin := c.Request.Header.Get("Origin")
|
|
|
|
|
if origin != "" {
|
|
|
|
|
// 设置允许的请求源
|
|
|
|
|
c.Header("Access-Control-Allow-Origin", origin)
|
|
|
|
|
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
|
|
|
|
//允许跨域设置可以返回其他子段,可以自定义字段
|
2023-09-05 11:47:03 +08:00
|
|
|
|
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, Chat-Token, Admin-Authorization")
|
2023-06-15 09:41:30 +08:00
|
|
|
|
// 允许浏览器(客户端)可以解析的头部 (重要)
|
|
|
|
|
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
|
|
|
|
//设置缓存时间
|
|
|
|
|
c.Header("Access-Control-Max-Age", "172800")
|
|
|
|
|
//允许客户端传递校验信息比如 cookie (重要)
|
|
|
|
|
c.Header("Access-Control-Allow-Credentials", "true")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if method == http.MethodOptions {
|
|
|
|
|
c.JSON(http.StatusOK, "ok!")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
|
if err := recover(); err != nil {
|
|
|
|
|
logger.Info("Panic info is: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
c.Next()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 用户授权验证
|
2023-09-05 11:47:03 +08:00
|
|
|
|
func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
2023-06-15 09:41:30 +08:00
|
|
|
|
return func(c *gin.Context) {
|
2023-09-05 11:47:03 +08:00
|
|
|
|
var tokenString string
|
2024-03-19 18:25:01 +08:00
|
|
|
|
isAdminApi := strings.Contains(c.Request.URL.Path, "/api/admin/")
|
|
|
|
|
if isAdminApi { // 后台管理 API
|
2023-09-05 11:47:03 +08:00
|
|
|
|
tokenString = c.GetHeader(types.AdminAuthHeader)
|
2023-12-14 16:48:54 +08:00
|
|
|
|
} else if c.Request.URL.Path == "/api/chat/new" {
|
2023-09-05 11:47:03 +08:00
|
|
|
|
tokenString = c.Query("token")
|
|
|
|
|
} else {
|
|
|
|
|
tokenString = c.GetHeader(types.UserAuthHeader)
|
|
|
|
|
}
|
2024-03-19 18:25:01 +08:00
|
|
|
|
|
2023-09-05 11:47:03 +08:00
|
|
|
|
if tokenString == "" {
|
2024-03-19 18:25:01 +08:00
|
|
|
|
if needLogin(c) {
|
|
|
|
|
resp.ERROR(c, "You should put Authorization in request headers")
|
|
|
|
|
c.Abort()
|
|
|
|
|
return
|
|
|
|
|
} else { // 直接放行
|
|
|
|
|
c.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-09-05 11:47:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
2024-03-21 11:04:12 +08:00
|
|
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok && needLogin(c) {
|
2023-09-05 11:47:03 +08:00
|
|
|
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
2023-06-15 09:41:30 +08:00
|
|
|
|
}
|
2024-03-19 18:25:01 +08:00
|
|
|
|
if isAdminApi {
|
|
|
|
|
return []byte(s.Config.AdminSession.SecretKey), nil
|
|
|
|
|
} else {
|
|
|
|
|
return []byte(s.Config.Session.SecretKey), nil
|
|
|
|
|
}
|
2023-09-05 11:47:03 +08:00
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
2024-03-20 21:11:52 +08:00
|
|
|
|
if err != nil && needLogin(c) {
|
2023-09-05 11:47:03 +08:00
|
|
|
|
resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err))
|
|
|
|
|
c.Abort()
|
2023-06-15 09:41:30 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
2023-09-05 11:47:03 +08:00
|
|
|
|
|
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
2024-03-20 21:11:52 +08:00
|
|
|
|
if !ok || !token.Valid && needLogin(c) {
|
2023-09-05 11:47:03 +08:00
|
|
|
|
resp.NotAuth(c, "Token is invalid")
|
|
|
|
|
c.Abort()
|
|
|
|
|
return
|
2023-06-19 07:06:59 +08:00
|
|
|
|
}
|
2023-09-05 11:47:03 +08:00
|
|
|
|
|
|
|
|
|
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
2024-03-20 21:11:52 +08:00
|
|
|
|
if expr > 0 && int64(expr) < time.Now().Unix() && needLogin(c) {
|
2023-09-05 11:47:03 +08:00
|
|
|
|
resp.NotAuth(c, "Token is expired")
|
2023-06-15 09:41:30 +08:00
|
|
|
|
c.Abort()
|
2023-09-05 11:47:03 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key := fmt.Sprintf("users/%v", claims["user_id"])
|
2024-03-21 15:24:28 +08:00
|
|
|
|
if isAdminApi {
|
|
|
|
|
key = fmt.Sprintf("admin/%v", claims["user_id"])
|
|
|
|
|
}
|
2024-03-19 18:25:01 +08:00
|
|
|
|
if _, err := client.Get(context.Background(), key).Result(); err != nil && needLogin(c) {
|
2023-09-05 11:47:03 +08:00
|
|
|
|
resp.NotAuth(c, "Token is not found in redis")
|
|
|
|
|
c.Abort()
|
|
|
|
|
return
|
2023-06-15 09:41:30 +08:00
|
|
|
|
}
|
2023-09-05 11:47:03 +08:00
|
|
|
|
c.Set(types.LoginUserID, claims["user_id"])
|
2023-06-15 09:41:30 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-23 17:50:55 +08:00
|
|
|
|
|
2024-03-19 18:25:01 +08:00
|
|
|
|
func needLogin(c *gin.Context) bool {
|
|
|
|
|
if c.Request.URL.Path == "/api/user/login" ||
|
|
|
|
|
c.Request.URL.Path == "/api/user/resetPass" ||
|
|
|
|
|
c.Request.URL.Path == "/api/admin/login" ||
|
|
|
|
|
c.Request.URL.Path == "/api/admin/login/captcha" ||
|
|
|
|
|
c.Request.URL.Path == "/api/user/register" ||
|
|
|
|
|
c.Request.URL.Path == "/api/chat/history" ||
|
|
|
|
|
c.Request.URL.Path == "/api/chat/detail" ||
|
|
|
|
|
c.Request.URL.Path == "/api/chat/list" ||
|
|
|
|
|
c.Request.URL.Path == "/api/role/list" ||
|
|
|
|
|
c.Request.URL.Path == "/api/model/list" ||
|
|
|
|
|
c.Request.URL.Path == "/api/mj/imgWall" ||
|
|
|
|
|
c.Request.URL.Path == "/api/mj/client" ||
|
|
|
|
|
c.Request.URL.Path == "/api/mj/notify" ||
|
|
|
|
|
c.Request.URL.Path == "/api/invite/hits" ||
|
|
|
|
|
c.Request.URL.Path == "/api/sd/imgWall" ||
|
|
|
|
|
c.Request.URL.Path == "/api/sd/client" ||
|
|
|
|
|
c.Request.URL.Path == "/api/config/get" ||
|
2024-03-19 18:59:02 +08:00
|
|
|
|
c.Request.URL.Path == "/api/product/list" ||
|
2024-03-19 18:25:01 +08:00
|
|
|
|
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
|
|
|
|
|
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
|
|
|
|
|
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
|
|
|
|
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
|
|
|
|
strings.HasPrefix(c.Request.URL.Path, "/api/payment/") ||
|
|
|
|
|
strings.HasPrefix(c.Request.URL.Path, "/static/") {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-23 17:50:55 +08:00
|
|
|
|
// 统一参数处理
|
|
|
|
|
func parameterHandlerMiddleware() gin.HandlerFunc {
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
// GET 参数处理
|
|
|
|
|
params := c.Request.URL.Query()
|
|
|
|
|
for key, values := range params {
|
|
|
|
|
for i, value := range values {
|
|
|
|
|
params[key][i] = strings.TrimSpace(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-30 17:35:56 +08:00
|
|
|
|
// update get parameters
|
2023-11-23 17:50:55 +08:00
|
|
|
|
c.Request.URL.RawQuery = params.Encode()
|
2023-11-30 17:35:56 +08:00
|
|
|
|
// skip file upload requests
|
2023-11-29 17:46:46 +08:00
|
|
|
|
contentType := c.Request.Header.Get("Content-Type")
|
|
|
|
|
if strings.Contains(contentType, "multipart/form-data") {
|
|
|
|
|
c.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-08 19:43:13 +08:00
|
|
|
|
if strings.Contains(contentType, "application/json") {
|
|
|
|
|
// process POST JSON request body
|
|
|
|
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-11-23 17:50:55 +08:00
|
|
|
|
|
2023-12-08 19:43:13 +08:00
|
|
|
|
// 还原请求体
|
|
|
|
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
|
|
|
// 将请求体解析为 JSON
|
|
|
|
|
var jsonData map[string]interface{}
|
|
|
|
|
if err := c.ShouldBindJSON(&jsonData); err != nil {
|
|
|
|
|
c.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-11-23 17:50:55 +08:00
|
|
|
|
|
2023-12-08 19:43:13 +08:00
|
|
|
|
// 对 JSON 数据中的字符串值去除两端空格
|
|
|
|
|
trimJSONStrings(jsonData)
|
|
|
|
|
// 更新请求体
|
|
|
|
|
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
|
|
|
|
|
}
|
2023-11-23 17:50:55 +08:00
|
|
|
|
|
|
|
|
|
c.Next()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 递归对 JSON 数据中的字符串值去除两端空格
|
|
|
|
|
func trimJSONStrings(data interface{}) {
|
|
|
|
|
switch v := data.(type) {
|
|
|
|
|
case map[string]interface{}:
|
|
|
|
|
for key, value := range v {
|
|
|
|
|
switch valueType := value.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
v[key] = strings.TrimSpace(valueType)
|
|
|
|
|
case map[string]interface{}, []interface{}:
|
|
|
|
|
trimJSONStrings(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case []interface{}:
|
|
|
|
|
for i, value := range v {
|
|
|
|
|
switch valueType := value.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
v[i] = strings.TrimSpace(valueType)
|
|
|
|
|
case map[string]interface{}, []interface{}:
|
|
|
|
|
trimJSONStrings(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-28 12:04:02 +08:00
|
|
|
|
|
|
|
|
|
// 静态资源中间件
|
|
|
|
|
func staticResourceMiddleware() gin.HandlerFunc {
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
|
|
|
|
|
url := c.Request.URL.String()
|
|
|
|
|
// 拦截生成缩略图请求
|
|
|
|
|
if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
|
|
|
|
|
r := strings.SplitAfter(url, "imageView2")
|
|
|
|
|
size := strings.Split(r[1], "/")
|
|
|
|
|
if len(size) != 8 {
|
|
|
|
|
c.String(http.StatusNotFound, "invalid thumb args")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
with := utils.IntValue(size[3], 0)
|
|
|
|
|
height := utils.IntValue(size[5], 0)
|
|
|
|
|
quality := utils.IntValue(size[7], 75)
|
|
|
|
|
|
|
|
|
|
// 打开图片文件
|
|
|
|
|
filePath := strings.TrimLeft(c.Request.URL.Path, "/")
|
|
|
|
|
file, err := os.Open(filePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.String(http.StatusNotFound, "Image not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
// 解码图片
|
|
|
|
|
img, _, err := image.Decode(file)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.String(http.StatusInternalServerError, "Error decoding image")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-28 14:50:19 +08:00
|
|
|
|
var newImg image.Image
|
|
|
|
|
if height == 0 || with == 0 {
|
|
|
|
|
// 固定宽度,高度自适应
|
|
|
|
|
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
|
|
|
|
|
} else {
|
|
|
|
|
// 生成缩略图
|
|
|
|
|
newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
|
|
|
|
|
}
|
2023-11-28 12:04:02 +08:00
|
|
|
|
var buffer bytes.Buffer
|
2023-11-28 14:50:19 +08:00
|
|
|
|
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
|
2023-11-28 12:04:02 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-21 15:00:46 +08:00
|
|
|
|
// 设置图片缓存有效期为一年 (365天)
|
|
|
|
|
c.Header("Cache-Control", "max-age=31536000, public")
|
2023-11-28 12:04:02 +08:00
|
|
|
|
// 直接输出图像数据流
|
|
|
|
|
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
|
2023-12-21 15:00:46 +08:00
|
|
|
|
c.Abort() // 中断请求
|
2023-11-28 12:04:02 +08:00
|
|
|
|
}
|
|
|
|
|
c.Next()
|
|
|
|
|
}
|
|
|
|
|
}
|