Compare commits
No commits in common. "582de60053bc992933b8e4eb7ee37eeccc0f220a" and "ff34d11c9b950755a41282edc441a04e1e4384a9" have entirely different histories.
582de60053
...
ff34d11c9b
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,17 +1,6 @@
|
|||||||
# CHANGELOG - go/service
|
# CHANGELOG - go/service
|
||||||
|
|
||||||
## v1.5.10 (2026-06-05)
|
## v1.5.6 (2026-06-05)
|
||||||
- **修复: Static 服务 URL 解码**:
|
|
||||||
- 修复了 \`service.Static()\` 在处理包含空格或特殊字符(如 \`%20\`)的请求路径时,因未解码导致文件匹配失败的问题。
|
|
||||||
- 在路由匹配阶段提前进行 URL Path 解码,提升整体路径匹配的健壮性。
|
|
||||||
|
|
||||||
## v1.5.9 (2026-06-05)
|
|
||||||
- **优化: 低代码环境深度对齐**:
|
|
||||||
- **JS 友好型 Header**: 引入 \`service.Header\` 包装类,提供大小写不敏感的 \`Get/Set/Add/Del\` 方法,提升脚本开发体验。
|
|
||||||
- **Cookie 遮蔽**: 在 \`Request\` 中实现方法遮蔽,确保 JS 侧看到的 Cookie 参数均为简化的 \`Service_Cookie\`,彻底解决穿透问题。
|
|
||||||
- **API 统一**: 将 \`Request.Headers()\` 重命名为 \`Request.Header()\`,与 \`Response.Header()\` 保持命名对齐。
|
|
||||||
- **重构**: 给内部字段(如 \`*http.Request\`, \`ResponseWriter\`)增加 \`js:"-"\` 标签,精准管控对 JS 暴露的 API 边界。
|
|
||||||
- **修复**: 解决了因 Header 包装导致的 Go 内部代码(\`handler.go\`, \`static.go\`)编译错误。
|
|
||||||
- **新特性: EnableWebDev 支持**:
|
- **新特性: EnableWebDev 支持**:
|
||||||
- 引入了 `service.EnableWebDev(config watch.Config)`,支持自动刷新页面的开发模式。
|
- 引入了 `service.EnableWebDev(config watch.Config)`,支持自动刷新页面的开发模式。
|
||||||
- **WebSocket 同步**: 自动注册 `/_watch` 服务,与文件监听器协同工作。
|
- **WebSocket 同步**: 自动注册 `/_watch` 服务,与文件监听器协同工作。
|
||||||
|
|||||||
19
handler.go
19
handler.go
@ -7,7 +7,6 @@ import (
|
|||||||
"apigo.cc/go/timer"
|
"apigo.cc/go/timer"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
@ -86,7 +85,7 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 过滤响应头
|
// 过滤响应头
|
||||||
respHeaders := make(map[string]string)
|
respHeaders := make(map[string]string)
|
||||||
for k, v := range response.Header().H {
|
for k, v := range response.Header() {
|
||||||
respHeaders[k] = strings.Join(v, ", ")
|
respHeaders[k] = strings.Join(v, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +148,7 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 路由匹配
|
// 3. 路由匹配
|
||||||
path, _ := url.PathUnescape(r.URL.Path)
|
path := r.URL.Path
|
||||||
host := r.Host
|
host := r.Host
|
||||||
|
|
||||||
// 处理静态文件
|
// 处理静态文件
|
||||||
@ -311,7 +310,7 @@ func parseRequestArgs(request *Request, args map[string]any) {
|
|||||||
|
|
||||||
// Form params
|
// Form params
|
||||||
if request.Method == http.MethodPost || request.Method == http.MethodPut {
|
if request.Method == http.MethodPost || request.Method == http.MethodPut {
|
||||||
contentType := request.Header().Get("Content-Type")
|
contentType := request.Header.Get("Content-Type")
|
||||||
if strings.HasPrefix(contentType, "application/json") {
|
if strings.HasPrefix(contentType, "application/json") {
|
||||||
body, _ := io.ReadAll(request.Body)
|
body, _ := io.ReadAll(request.Body)
|
||||||
_ = request.Body.Close()
|
_ = request.Body.Close()
|
||||||
@ -431,9 +430,9 @@ func outputResult(response *Response, result any) {
|
|||||||
func (ws *webServer) handleClientKeys(request *Request, response *Response) {
|
func (ws *webServer) handleClientKeys(request *Request, response *Response) {
|
||||||
// SessionId
|
// SessionId
|
||||||
if ws.usedSessionIdKey != "" {
|
if ws.usedSessionIdKey != "" {
|
||||||
sessionId := request.Header().Get(ws.usedSessionIdKey)
|
sessionId := request.Header.Get(ws.usedSessionIdKey)
|
||||||
if sessionId == "" && !ws.Config.SessionWithoutCookie {
|
if sessionId == "" && !ws.Config.SessionWithoutCookie {
|
||||||
if ck := request.GetCookie(ws.usedSessionIdKey); ck != nil {
|
if ck, err := request.Cookie(ws.usedSessionIdKey); err == nil {
|
||||||
sessionId = ck.Value
|
sessionId = ck.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -452,15 +451,15 @@ func (ws *webServer) handleClientKeys(request *Request, response *Response) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.Request.Header.Set(discover.HeaderSessionID, sessionId)
|
request.Header.Set(discover.HeaderSessionID, sessionId)
|
||||||
response.Header().Set(ws.usedSessionIdKey, sessionId)
|
response.Header().Set(ws.usedSessionIdKey, sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceId
|
// DeviceId
|
||||||
if ws.usedDeviceIdKey != "" {
|
if ws.usedDeviceIdKey != "" {
|
||||||
deviceId := request.Header().Get(ws.usedDeviceIdKey)
|
deviceId := request.Header.Get(ws.usedDeviceIdKey)
|
||||||
if deviceId == "" && !ws.Config.DeviceWithoutCookie {
|
if deviceId == "" && !ws.Config.DeviceWithoutCookie {
|
||||||
if ck := request.GetCookie(ws.usedDeviceIdKey); ck != nil {
|
if ck, err := request.Cookie(ws.usedDeviceIdKey); err == nil {
|
||||||
deviceId = ck.Value
|
deviceId = ck.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -476,7 +475,7 @@ func (ws *webServer) handleClientKeys(request *Request, response *Response) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.Request.Header.Set(discover.HeaderDeviceID, deviceId)
|
request.Header.Set(discover.HeaderDeviceID, deviceId)
|
||||||
response.Header().Set(ws.usedDeviceIdKey, deviceId)
|
response.Header().Set(ws.usedDeviceIdKey, deviceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
js_export.go
12
js_export.go
@ -6,12 +6,12 @@ import (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
jsmod.Register("service", map[string]any{
|
jsmod.Register("service", map[string]any{
|
||||||
// 类型占位工厂 (用于 AI 发现类型结构,生成文档时隐藏)
|
// 类型占位工厂 (用于 AI 发现类型结构)
|
||||||
"__exportRequest": func() *Request { return &Request{} },
|
"newRequest": func() *Request { return &Request{} },
|
||||||
"__exportResponse": func() *Response { return &Response{} },
|
"newResponse": func() *Response { return &Response{} },
|
||||||
"__exportWebSocket": func() *WebSocketConn { return &WebSocketConn{} },
|
"newWebSocket": func() *WebSocketConn { return &WebSocketConn{} },
|
||||||
"__exportSession": func() *Session { return &Session{} },
|
"newSession": func() *Session { return &Session{} },
|
||||||
"__exportFile": func() *jsUploadFile { return &jsUploadFile{} },
|
"newFile": func() *jsUploadFile { return &jsUploadFile{} },
|
||||||
|
|
||||||
// 功能函数
|
// 功能函数
|
||||||
"upgrade": Upgrade,
|
"upgrade": Upgrade,
|
||||||
|
|||||||
125
request.go
125
request.go
@ -33,120 +33,13 @@ func (f *UploadFile) Content() ([]byte, error) {
|
|||||||
return io.ReadAll(src)
|
return io.ReadAll(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header 包装 http.Header 以提供 JS 友好的方法
|
|
||||||
type Header struct {
|
|
||||||
H http.Header `js:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Header) Get(key string) string {
|
|
||||||
return h.H.Get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Header) Set(key, value string) {
|
|
||||||
h.H.Set(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Header) Add(key, value string) {
|
|
||||||
h.H.Add(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Header) Del(key string) {
|
|
||||||
h.H.Del(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Header) Values(key string) []string {
|
|
||||||
return h.H.Values(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request 封装 http.Request
|
// Request 封装 http.Request
|
||||||
type Request struct {
|
type Request struct {
|
||||||
*http.Request `js:"-"`
|
*http.Request
|
||||||
contextValues map[string]any `js:"-"`
|
contextValues map[string]any
|
||||||
Id string
|
Id string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookie 简化的 JS 友好 Cookie 结构
|
|
||||||
type Cookie struct {
|
|
||||||
Name string
|
|
||||||
Value string
|
|
||||||
Path string
|
|
||||||
Domain string
|
|
||||||
MaxAge int
|
|
||||||
Secure bool
|
|
||||||
HttpOnly bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) GetCookie(name string) *Cookie {
|
|
||||||
c, err := r.Request.Cookie(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &Cookie{
|
|
||||||
Name: c.Name,
|
|
||||||
Value: c.Value,
|
|
||||||
Path: c.Path,
|
|
||||||
Domain: c.Domain,
|
|
||||||
MaxAge: c.MaxAge,
|
|
||||||
Secure: c.Secure,
|
|
||||||
HttpOnly: c.HttpOnly,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) GetCookies() []*Cookie {
|
|
||||||
res := make([]*Cookie, 0)
|
|
||||||
for _, c := range r.Request.Cookies() {
|
|
||||||
res = append(res, &Cookie{
|
|
||||||
Name: c.Name,
|
|
||||||
Value: c.Value,
|
|
||||||
Path: c.Path,
|
|
||||||
Domain: c.Domain,
|
|
||||||
MaxAge: c.MaxAge,
|
|
||||||
Secure: c.Secure,
|
|
||||||
HttpOnly: c.HttpOnly,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddCookie 遮蔽原生的 AddCookie
|
|
||||||
func (r *Request) AddCookie(c *Cookie) {
|
|
||||||
if c == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.Request.AddCookie(&http.Cookie{
|
|
||||||
Name: c.Name,
|
|
||||||
Value: c.Value,
|
|
||||||
Path: c.Path,
|
|
||||||
Domain: c.Domain,
|
|
||||||
MaxAge: c.MaxAge,
|
|
||||||
Secure: c.Secure,
|
|
||||||
HttpOnly: c.HttpOnly,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CookiesNamed 遮蔽原生的 CookiesNamed
|
|
||||||
func (r *Request) CookiesNamed(name string) []*Cookie {
|
|
||||||
res := make([]*Cookie, 0)
|
|
||||||
for _, c := range r.Request.Cookies() {
|
|
||||||
if c.Name == name {
|
|
||||||
res = append(res, &Cookie{
|
|
||||||
Name: c.Name,
|
|
||||||
Value: c.Value,
|
|
||||||
Path: c.Path,
|
|
||||||
Domain: c.Domain,
|
|
||||||
MaxAge: c.MaxAge,
|
|
||||||
Secure: c.Secure,
|
|
||||||
HttpOnly: c.HttpOnly,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) Header() *Header {
|
|
||||||
return &Header{H: r.Request.Header}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRequest 创建 Request 包装
|
// NewRequest 创建 Request 包装
|
||||||
func NewRequest(httpRequest *http.Request) *Request {
|
func NewRequest(httpRequest *http.Request) *Request {
|
||||||
return &Request{
|
return &Request{
|
||||||
@ -175,11 +68,11 @@ func (r *Request) Get(key string) any {
|
|||||||
|
|
||||||
// MakeUrl 根据当前请求构建完整 URL
|
// MakeUrl 根据当前请求构建完整 URL
|
||||||
func (r *Request) MakeUrl(path string) string {
|
func (r *Request) MakeUrl(path string) string {
|
||||||
scheme := r.Header().Get(discover.HeaderScheme)
|
scheme := r.Header.Get(discover.HeaderScheme)
|
||||||
if scheme == "" {
|
if scheme == "" {
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
}
|
}
|
||||||
host := r.Header().Get(discover.HeaderHost)
|
host := r.Header.Get(discover.HeaderHost)
|
||||||
if host == "" {
|
if host == "" {
|
||||||
host = r.Host
|
host = r.Host
|
||||||
}
|
}
|
||||||
@ -188,24 +81,24 @@ func (r *Request) MakeUrl(path string) string {
|
|||||||
|
|
||||||
// DeviceId 获取设备 ID
|
// DeviceId 获取设备 ID
|
||||||
func (r *Request) DeviceId() string {
|
func (r *Request) DeviceId() string {
|
||||||
return r.Header().Get(discover.HeaderDeviceID)
|
return r.Header.Get(discover.HeaderDeviceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionId 获取会话 ID
|
// SessionId 获取会话 ID
|
||||||
func (r *Request) SessionId() string {
|
func (r *Request) SessionId() string {
|
||||||
return r.Header().Get(discover.HeaderSessionID)
|
return r.Header.Get(discover.HeaderSessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUserId 设置用户 ID(传递给下游)
|
// SetUserId 设置用户 ID(传递给下游)
|
||||||
func (r *Request) SetUserId(userId string) {
|
func (r *Request) SetUserId(userId string) {
|
||||||
r.Header().Set(discover.HeaderUserID, userId)
|
r.Header.Set(discover.HeaderUserID, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientIp 获取真实 IP
|
// ClientIp 获取真实 IP
|
||||||
func (r *Request) ClientIp() string {
|
func (r *Request) ClientIp() string {
|
||||||
ip := r.Header().Get(discover.HeaderClientIP)
|
ip := r.Header.Get(discover.HeaderClientIP)
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
ip = r.Header().Get(discover.HeaderForwardedFor)
|
ip = r.Header.Get(discover.HeaderForwardedFor)
|
||||||
}
|
}
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
|||||||
39
response.go
39
response.go
@ -10,31 +10,16 @@ import (
|
|||||||
// Response 封装 http.ResponseWriter
|
// Response 封装 http.ResponseWriter
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Id string
|
Id string
|
||||||
Writer http.ResponseWriter `js:"-"`
|
Writer http.ResponseWriter
|
||||||
Code int
|
Code int
|
||||||
body []byte `js:"-"`
|
body []byte
|
||||||
outLen int `js:"-"`
|
outLen int
|
||||||
changed bool `js:"-"`
|
changed bool
|
||||||
headerWritten bool `js:"-"`
|
headerWritten bool
|
||||||
dontLog200 bool `js:"-"`
|
dontLog200 bool
|
||||||
dontLogArgs []string `js:"-"`
|
dontLogArgs []string
|
||||||
ProxyHeader *http.Header `js:"-"`
|
ProxyHeader *http.Header
|
||||||
server *webServer `js:"-"`
|
server *webServer
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Response) SetCookie(cookie *Cookie) {
|
|
||||||
if cookie == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.SetCookie(r.Writer, &http.Cookie{
|
|
||||||
Name: cookie.Name,
|
|
||||||
Value: cookie.Value,
|
|
||||||
Path: cookie.Path,
|
|
||||||
Domain: cookie.Domain,
|
|
||||||
MaxAge: cookie.MaxAge,
|
|
||||||
Secure: cookie.Secure,
|
|
||||||
HttpOnly: cookie.HttpOnly,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewResponse 创建 Response 包装
|
// NewResponse 创建 Response 包装
|
||||||
@ -47,11 +32,11 @@ func NewResponse(writer http.ResponseWriter, server *webServer) *Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Header 获取响应头部
|
// Header 获取响应头部
|
||||||
func (r *Response) Header() *Header {
|
func (r *Response) Header() http.Header {
|
||||||
if r.ProxyHeader != nil {
|
if r.ProxyHeader != nil {
|
||||||
return &Header{H: *r.ProxyHeader}
|
return *r.ProxyHeader
|
||||||
}
|
}
|
||||||
return &Header{H: r.Writer.Header()}
|
return r.Writer.Header()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write 写入响应内容
|
// Write 写入响应内容
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"apigo.cc/go/log"
|
"apigo.cc/go/log"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -77,7 +76,6 @@ func (ws *webServer) ReplaceStatics(host string, config map[string]string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ws *webServer) getStaticFilePath(requestPath, host string) string {
|
func (ws *webServer) getStaticFilePath(requestPath, host string) string {
|
||||||
requestPath, _ = url.PathUnescape(requestPath)
|
|
||||||
ws.staticsByHostLock.RLock()
|
ws.staticsByHostLock.RLock()
|
||||||
defer ws.staticsByHostLock.RUnlock()
|
defer ws.staticsByHostLock.RUnlock()
|
||||||
|
|
||||||
@ -134,7 +132,7 @@ func (ws *webServer) processStatic(requestPath string, request *Request, respons
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查 304
|
// 检查 304
|
||||||
if ifModifiedSince := request.Header().Get("If-Modified-Since"); ifModifiedSince != "" {
|
if ifModifiedSince := request.Header.Get("If-Modified-Since"); ifModifiedSince != "" {
|
||||||
if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {
|
if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {
|
||||||
if time.Unix(info.ModTime, 0).Truncate(time.Second).Before(t.Truncate(time.Second)) ||
|
if time.Unix(info.ModTime, 0).Truncate(time.Second).Before(t.Truncate(time.Second)) ||
|
||||||
time.Unix(info.ModTime, 0).Truncate(time.Second).Equal(t.Truncate(time.Second)) {
|
time.Unix(info.ModTime, 0).Truncate(time.Second).Equal(t.Truncate(time.Second)) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user