fun: HTTP RPC framework with DI, guards, streams and codegen
- Service binding via reflection with 4 method signatures - Dependency injection (Wired/auto tags) and guards - Streamable HTTP (NDJSON) streaming responses - Go/TypeScript client code generation (self-contained, with interceptors) - Enum support, structured Result, logging - 16 regression tests covering all fixed bugs
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Binaries
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Generated code
|
||||
gen/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Dependencies
|
||||
vendor/
|
||||
node_modules/
|
||||
@@ -0,0 +1,159 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Wired 创建并注册一个依赖实例;auto 标签字段递归注入依赖;存在 New() 则调用
|
||||
func Wired[T any]() *T {
|
||||
t := reflect.TypeFor[T]()
|
||||
data := new(T)
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic("Fun: " + t.Name() + " It must be a structure")
|
||||
}
|
||||
if isPrivate(t.Name()) {
|
||||
panic("Fun:" + t.Name() + " cannot be Private")
|
||||
}
|
||||
if newMethod, found := t.MethodByName("New"); found {
|
||||
if newMethod.Type.NumIn() != 1 || newMethod.Type.NumOut() != 0 {
|
||||
panic("Fun:" + t.Name() + " New method must have no parameters and no return values")
|
||||
}
|
||||
}
|
||||
f := GetFun()
|
||||
if box, isWired := f.boxes.Load(reflect.TypeFor[*T]()); isWired {
|
||||
return box.(reflect.Value).Interface().(*T)
|
||||
}
|
||||
v := reflect.ValueOf(data)
|
||||
f.boxes.Store(reflect.TypeFor[*T](), v)
|
||||
boxList := map[reflect.Type]bool{}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
c := t.Field(i)
|
||||
fieldTag := newTag(c.Tag)
|
||||
if _, isAuto := fieldTag.getTag("auto"); isAuto {
|
||||
if dependency, loaded := f.boxes.Load(c.Type); loaded {
|
||||
v.Elem().Field(i).Set(dependency.(reflect.Value))
|
||||
} else {
|
||||
checkBox(c, boxList)
|
||||
f.autowired(v.Elem().Field(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
newMethod := v.MethodByName("New")
|
||||
if newMethod.IsValid() {
|
||||
newMethod.Call(nil)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// autowired 递归创建依赖实例并注入 auto 标签字段
|
||||
func (f *Fun) autowired(fieldValue reflect.Value) {
|
||||
instance := reflect.New(fieldValue.Type().Elem())
|
||||
f.boxes.Store(fieldValue.Type(), instance)
|
||||
fieldValue.Set(instance)
|
||||
structValue := instance.Elem()
|
||||
for i := 0; i < structValue.NumField(); i++ {
|
||||
structField := structValue.Type().Field(i)
|
||||
fieldTag := newTag(structField.Tag)
|
||||
if _, isAuto := fieldTag.getTag("auto"); isAuto {
|
||||
if dependency, loaded := f.boxes.Load(structField.Type); loaded {
|
||||
structValue.Field(i).Set(dependency.(reflect.Value))
|
||||
} else {
|
||||
f.autowired(structValue.Field(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
newMethod := instance.MethodByName("New")
|
||||
if newMethod.IsValid() {
|
||||
newMethod.Call(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// checkBox 校验 auto 注入字段:必须是指针+struct、非匿名、非私有;New() 必须无参无返回值
|
||||
func checkBox(s reflect.StructField, boxList map[reflect.Type]bool) {
|
||||
if _, ok := boxList[s.Type]; ok {
|
||||
return
|
||||
}
|
||||
boxList[s.Type] = true
|
||||
if s.Anonymous {
|
||||
panic("Fun:" + s.Name + " cannot be Anonymous")
|
||||
}
|
||||
if s.Type.Kind() != reflect.Ptr || s.Type.Elem().Kind() != reflect.Struct {
|
||||
panic("Fun:" + s.Name + " Must be a pointer and a struct")
|
||||
}
|
||||
if isPrivate(s.Name) {
|
||||
panic("Fun:" + s.Name + " cannot be Private")
|
||||
}
|
||||
if newMethod, found := s.Type.MethodByName("New"); found {
|
||||
if newMethod.Type.NumIn() != 1 || newMethod.Type.NumOut() != 0 {
|
||||
panic("Fun:" + s.Name + " New method must have no parameters and no return values")
|
||||
}
|
||||
}
|
||||
for i := 0; i < s.Type.Elem().NumField(); i++ {
|
||||
f := s.Type.Elem().Field(i)
|
||||
fieldTag := newTag(f.Tag)
|
||||
if _, isAuto := fieldTag.getTag("auto"); isAuto {
|
||||
checkBox(f, boxList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// boxWired 注册期预初始化服务结构体字段中的 Box 依赖
|
||||
func boxWired(service any, f *Fun) {
|
||||
serviceInstance := reflect.New(reflect.TypeOf(service).Elem()).Elem()
|
||||
for i := 0; i < serviceInstance.NumField(); i++ {
|
||||
field := serviceInstance.Field(i)
|
||||
if field.Type() == ctxType {
|
||||
continue
|
||||
}
|
||||
if field.Type().Kind() == reflect.Ptr && field.Type().Elem().Kind() == reflect.Struct {
|
||||
if _, isWired := f.boxes.Load(field.Type()); !isWired {
|
||||
f.autowired(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serviceWired 每请求把 Ctx 与 Box 依赖注入到新创建的服务实例
|
||||
func (f *Fun) serviceWired(serviceInstance reflect.Value, ctx *Ctx) {
|
||||
for i := 0; i < serviceInstance.NumField(); i++ {
|
||||
field := serviceInstance.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
if field.Type() == ctxType {
|
||||
field.Set(reflect.ValueOf(*ctx))
|
||||
} else if dependency, ok := f.boxes.Load(field.Type()); ok {
|
||||
field.Set(dependency.(reflect.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkGuard 校验 Guard 类型:必须是指向结构体的指针
|
||||
func checkGuard(guard Guard) {
|
||||
t := reflect.TypeOf(guard)
|
||||
if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
|
||||
panic("Fun: guard must be a pointer to a struct")
|
||||
}
|
||||
if isPrivate(t.Elem().Name()) {
|
||||
panic("Fun:" + t.Elem().Name() + " cannot be Private")
|
||||
}
|
||||
}
|
||||
|
||||
// serviceGuardWired 创建 Guard 实例并注入 Box 依赖,返回 guard 引用
|
||||
func serviceGuardWired(guard Guard, f *Fun) *any {
|
||||
t := reflect.TypeOf(guard).Elem()
|
||||
guardInstance := reflect.New(t).Elem()
|
||||
for i := 0; i < guardInstance.NumField(); i++ {
|
||||
field := guardInstance.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
if dependency, ok := f.boxes.Load(field.Type()); ok {
|
||||
field.Set(dependency.(reflect.Value))
|
||||
} else {
|
||||
f.autowired(field)
|
||||
}
|
||||
}
|
||||
g := guardInstance.Addr().Interface()
|
||||
return &g
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BugStatus uint8
|
||||
|
||||
func (BugStatus) Names() []string { return []string{"A", "B"} }
|
||||
|
||||
type BugDto struct {
|
||||
Status *BugStatus
|
||||
}
|
||||
|
||||
type BugSvc struct{}
|
||||
|
||||
func (s *BugSvc) Ping() error { return nil }
|
||||
|
||||
func (s *BugSvc) Save(dto BugDto) (string, error) { return "ok", nil }
|
||||
|
||||
func (s *BugSvc) Ticker() (string, *Stream, error) {
|
||||
st := Stream{}
|
||||
go func() {
|
||||
st.Send("tick")
|
||||
st.Close()
|
||||
}()
|
||||
return "first", &st, nil
|
||||
}
|
||||
|
||||
func bugInvoke(t *testing.T, method string, data map[string]any) (*Result[any], error) {
|
||||
t.Helper()
|
||||
f := New()
|
||||
f.BindService(&BugSvc{})
|
||||
if data == nil {
|
||||
data = map[string]any{}
|
||||
}
|
||||
c := &Ctx{Ip: "1", MethodName: method, ServiceName: "BugSvc", Data: &data}
|
||||
var streamCh chan any
|
||||
var streamDone chan struct{}
|
||||
return f.invoke(c, &streamCh, &streamDone)
|
||||
}
|
||||
|
||||
// bug1: () error 签名的方法 invoke 应返回空数据结果而不是越界 panic
|
||||
func TestBugErrorOnlyInvoke(t *testing.T) {
|
||||
res, err := bugInvoke(t, "Ping", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if res.Data != nil {
|
||||
t.Fatalf("expect nil data, got %v", *res.Data)
|
||||
}
|
||||
}
|
||||
|
||||
// bug2: 指针枚举字段传 null 应放行;越界值仍要报错
|
||||
func TestBugNullableEnum(t *testing.T) {
|
||||
if _, err := bugInvoke(t, "Save", map[string]any{"status": nil}); err != nil {
|
||||
t.Fatalf("nullable enum should pass: %v", err)
|
||||
}
|
||||
if _, err := bugInvoke(t, "Save", map[string]any{"status": 1}); err != nil {
|
||||
t.Fatalf("valid enum should pass: %v", err)
|
||||
}
|
||||
if _, err := bugInvoke(t, "Save", map[string]any{"status": 5}); err == nil {
|
||||
t.Fatal("out-of-range enum should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// bug3: 含 () error 方法的代码生成不应 panic,且类型应生成为 Void/void
|
||||
func TestBugGenErrorOnly(t *testing.T) {
|
||||
GetFun().BindService(&BugSvc{})
|
||||
SetOutput(t.TempDir())
|
||||
GenCode(GenGo{}, GenTs{})
|
||||
goSrc, err := os.ReadFile(filepath.Join(getDirectory(), "go", "bug_svc.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("go file missing: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(goSrc), "Result[Void]") {
|
||||
t.Fatalf("go: expect Result[Void], got:\n%s", goSrc)
|
||||
}
|
||||
tsSrc, err := os.ReadFile(filepath.Join(getDirectory(), "ts", "bugSvc.ts"))
|
||||
if err != nil {
|
||||
t.Fatalf("ts file missing: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(tsSrc), "result<void>") {
|
||||
t.Fatalf("ts: expect result<void>, got:\n%s", tsSrc)
|
||||
}
|
||||
}
|
||||
|
||||
// bug4+5: 响应键应为小写;(T, stream, error) 的 T 应作为流的第一条消息下发
|
||||
func TestBugJsonKeysAndStreamFirst(t *testing.T) {
|
||||
f := New()
|
||||
f.BindService(&BugSvc{})
|
||||
go f.Start(39003)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
resp, err := http.Post("http://127.0.0.1:39003/cell", "application/json",
|
||||
strings.NewReader(`{"serviceName":"BugSvc","methodName":"Ping"}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
body := buf.String()
|
||||
if !strings.Contains(body, `"status"`) || strings.Contains(body, `"Status"`) {
|
||||
t.Fatalf("keys not lowercase: %s", body)
|
||||
}
|
||||
|
||||
resp2, err := http.Post("http://127.0.0.1:39003/cell", "application/json",
|
||||
bytes.NewReader([]byte(`{"serviceName":"BugSvc","methodName":"Ticker"}`)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
var got []string
|
||||
scanner := bufio.NewScanner(resp2.Body)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var msg string
|
||||
if err := json.Unmarshal([]byte(line), &msg); err != nil {
|
||||
t.Fatalf("bad ndjson line %q: %v", line, err)
|
||||
}
|
||||
got = append(got, msg)
|
||||
}
|
||||
if len(got) != 2 || got[0] != "first" || got[1] != "tick" {
|
||||
t.Fatalf("stream: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// bug7: 非指针切片字段传 null 应放行(JSON null -> nil slice)
|
||||
type NullSlicDto struct {
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type NullSlicSvc struct{}
|
||||
|
||||
func (s *NullSlicSvc) Save(dto NullSlicDto) (string, error) { return "ok", nil }
|
||||
|
||||
func TestBugSliceNull(t *testing.T) {
|
||||
f := New()
|
||||
f.BindService(&NullSlicSvc{})
|
||||
data := map[string]any{"tags": nil}
|
||||
c := &Ctx{Ip: "1", MethodName: "Save", ServiceName: "NullSlicSvc", Data: &data}
|
||||
var streamCh chan any
|
||||
var streamDone chan struct{}
|
||||
if _, err := f.invoke(c, &streamCh, &streamDone); err != nil {
|
||||
t.Fatalf("slice null should pass: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// bug8: 业务方法返回 error 但已启动 goroutine 调 Send,框架应注入取消流让 goroutine 退出而非挂死
|
||||
var leakDone chan struct{}
|
||||
|
||||
type LeakSvc struct{}
|
||||
|
||||
func (s *LeakSvc) Fail() (*Stream, error) {
|
||||
st := Stream{}
|
||||
leakDone = make(chan struct{})
|
||||
go func() {
|
||||
st.Send("never")
|
||||
close(leakDone)
|
||||
}()
|
||||
return &st, errors.New("boom")
|
||||
}
|
||||
|
||||
func TestBugStreamLeak(t *testing.T) {
|
||||
f := New()
|
||||
f.BindService(&LeakSvc{})
|
||||
c := &Ctx{Ip: "1", MethodName: "Fail", ServiceName: "LeakSvc"}
|
||||
var streamCh chan any
|
||||
var streamDone chan struct{}
|
||||
_, err := f.invoke(c, &streamCh, &streamDone)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
// Send goroutine 必须解除阻塞(有超时保护,防挂死)
|
||||
select {
|
||||
case <-leakDone:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Send goroutine blocked forever: stream leak")
|
||||
}
|
||||
}
|
||||
|
||||
// bug6: 与 fasthttp.RequestCtx 方法重名的用户方法不应被静默丢弃
|
||||
type CollideSvc struct {
|
||||
Ctx
|
||||
}
|
||||
|
||||
func (s *CollideSvc) Cookie() (string, error) { return "cookie", nil }
|
||||
|
||||
func TestBugMethodNameCollision(t *testing.T) {
|
||||
f := New()
|
||||
f.BindService(&CollideSvc{})
|
||||
if _, ok := f.methods["CollideSvc.Cookie"]; !ok {
|
||||
t.Fatal("Cookie method dropped due to name collision with fasthttp.RequestCtx")
|
||||
}
|
||||
res, err := func() (*Result[any], error) {
|
||||
c := &Ctx{Ip: "1", MethodName: "Cookie", ServiceName: "CollideSvc"}
|
||||
data := map[string]any{}
|
||||
c.Data = &data
|
||||
var streamCh chan any
|
||||
var streamDone chan struct{}
|
||||
return f.invoke(c, &streamCh, &streamDone)
|
||||
}()
|
||||
if err != nil {
|
||||
t.Fatalf("invoke Cookie err: %v", err)
|
||||
}
|
||||
if (*res.Data).(string) != "cookie" {
|
||||
t.Fatalf("unexpected: %v", *res.Data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func isPrivate(value string) bool {
|
||||
return !unicode.IsUpper([]rune(value)[0])
|
||||
}
|
||||
|
||||
// checkType 注册期递归校验类型是否受支持:
|
||||
// int/uint/string/bool/struct/slice/enum;不支持匿名结构体、私有类型、空结构体;
|
||||
// 枚举要求 Names/DisplayNames 长度一致
|
||||
func checkType(t reflect.Type) {
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
if strings.Contains(t.String(), "{}") {
|
||||
panic(fmt.Sprintf("fun: %s generic types containing 'any' or interface{} are not supported", t.Name()))
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.String, reflect.Bool:
|
||||
if t.Kind() == reflect.Uint8 && (t.Implements(displayEnumType) || t.Implements(enumType)) && isPrivate(t.Name()) {
|
||||
panic("fun:" + t.Name() + " cannot be Private")
|
||||
}
|
||||
if t.Kind() == reflect.Uint8 && t.Implements(displayEnumType) {
|
||||
enumValue := reflect.New(t).Elem().Interface().(displayEnum)
|
||||
if len(enumValue.DisplayNames()) != len(enumValue.Names()) {
|
||||
panic("fun: " + t.Name() + " enum names and display names must be the same length")
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
if t.NumField() == 0 {
|
||||
panic("fun: " + t.Name() + " must have at least one field")
|
||||
}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
if isPrivate(f.Name) {
|
||||
panic("fun:" + f.Name + " cannot be Private")
|
||||
}
|
||||
checkType(f.Type)
|
||||
}
|
||||
case reflect.Slice:
|
||||
checkType(t.Elem())
|
||||
default:
|
||||
panic("fun:Unsupported types " + t.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// checkDto 运行时校验请求数据:
|
||||
// 非指针字段必须出现在请求中且非 nil;嵌套 struct/slice 递归;枚举值必须在范围内
|
||||
func checkDto(dtoType reflect.Type, dtoMap any, methodName string) error {
|
||||
t := dtoType
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
obj, ok := dtoMap.(map[string]any)
|
||||
if !ok {
|
||||
return callError(fmt.Errorf("fun: method %s DTO %s must be an object", methodName, t.Name()))
|
||||
}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
value, ok := obj[firstLetterToLower(f.Name)]
|
||||
if !ok {
|
||||
// 兼容按原始字段名(首字母大写)传参的客户端
|
||||
value, ok = obj[f.Name]
|
||||
}
|
||||
// 非指针且非切片字段必须存在且非 null;切片字段允许 null(反序列化为 nil slice)
|
||||
if f.Type.Kind() != reflect.Ptr && f.Type.Kind() != reflect.Slice && (!ok || value == nil) {
|
||||
return callError(fmt.Errorf("fun: %s Dto must be a pointer or have a corresponding field in the map", f.Name))
|
||||
}
|
||||
ft := f.Type
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if (ft.Kind() == reflect.Struct || ft.Kind() == reflect.Slice) && value != nil {
|
||||
if err := checkDto(ft, value, methodName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if ft.Kind() == reflect.Uint8 && value != nil && (ft.Implements(displayEnumType) || ft.Implements(enumType)) {
|
||||
if err := checkEnumValue(ft, value, f.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
list, ok := dtoMap.([]any)
|
||||
if !ok {
|
||||
return callError(fmt.Errorf("fun: Dto must be an array"))
|
||||
}
|
||||
for _, value := range list {
|
||||
et0 := t.Elem()
|
||||
et := et0
|
||||
if et.Kind() == reflect.Ptr {
|
||||
et = et.Elem()
|
||||
}
|
||||
// 指针元素允许 null,值元素必须非空
|
||||
if et0.Kind() != reflect.Ptr && value == nil {
|
||||
return callError(fmt.Errorf("fun:%s Dto must be a pointer or have a corresponding field in the map", et0.Name()))
|
||||
}
|
||||
if (et.Kind() == reflect.Struct || et.Kind() == reflect.Slice) && value != nil {
|
||||
if err := checkDto(et, value, methodName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if et.Kind() == reflect.Uint8 && value != nil && (et.Implements(displayEnumType) || et.Implements(enumType)) {
|
||||
if err := checkEnumValue(et, value, et.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkEnumValue 运行时校验枚举值是否在范围内
|
||||
func checkEnumValue(t reflect.Type, value any, name string) error {
|
||||
var max uint8
|
||||
enumValue := reflect.New(t).Elem()
|
||||
if t.Implements(displayEnumType) {
|
||||
max = uint8(len(enumValue.Interface().(displayEnum).Names()))
|
||||
} else {
|
||||
max = uint8(len(enumValue.Interface().(enum).Names()))
|
||||
}
|
||||
var num uint8
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
num = uint8(v)
|
||||
case float32:
|
||||
num = uint8(v)
|
||||
case uint8:
|
||||
num = v
|
||||
case uint16:
|
||||
num = uint8(v)
|
||||
case uint32:
|
||||
num = uint8(v)
|
||||
case uint64:
|
||||
num = uint8(v)
|
||||
case int:
|
||||
num = uint8(v)
|
||||
case int8:
|
||||
num = uint8(v)
|
||||
case int16:
|
||||
num = uint8(v)
|
||||
case int32:
|
||||
num = uint8(v)
|
||||
case int64:
|
||||
num = uint8(v)
|
||||
default:
|
||||
return callError(errors.New("Fun:" + name + " Dto enum value type is not supported"))
|
||||
}
|
||||
if num >= max {
|
||||
return callError(errors.New("Fun:" + name + " Dto value out of range"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"reflect"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// Ctx 请求上下文。
|
||||
// 命名持有 *fasthttp.RequestCtx(非嵌入),避免其方法集提升到服务上,
|
||||
// 辅助方法全部小写,保证服务方法集只含业务方法。
|
||||
type Ctx struct {
|
||||
Ip string
|
||||
State map[string]string
|
||||
MethodName string
|
||||
ServiceName string
|
||||
Data *map[string]any
|
||||
RequestCtx *fasthttp.RequestCtx
|
||||
}
|
||||
|
||||
var ctxType = reflect.TypeFor[Ctx]()
|
||||
|
||||
func (c *Ctx) path() string { return string(c.RequestCtx.Path()) }
|
||||
func (c *Ctx) isPost() bool { return c.RequestCtx.IsPost() }
|
||||
func (c *Ctx) postBody() []byte { return c.RequestCtx.PostBody() }
|
||||
func (c *Ctx) remoteIP() net.IP { return c.RequestCtx.RemoteIP() }
|
||||
func (c *Ctx) setStatusCode(code int) { c.RequestCtx.SetStatusCode(code) }
|
||||
func (c *Ctx) write(p []byte) (int, error) { return c.RequestCtx.Write(p) }
|
||||
|
||||
// send 写回响应,嵌套对象键统一转小写(兼容 TS/大小写敏感客户端)
|
||||
func (c *Ctx) send(result Result[any]) {
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
c.sendError(err)
|
||||
return
|
||||
}
|
||||
raw, err := lowerKeysFromJSON(data)
|
||||
if err != nil {
|
||||
_, _ = c.write(data)
|
||||
return
|
||||
}
|
||||
out, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
_, _ = c.write(data)
|
||||
return
|
||||
}
|
||||
_, _ = c.write(out)
|
||||
}
|
||||
|
||||
// lowerKeysFromJSON 解析 JSON 后递归把所有对象键转为首字母小写
|
||||
func lowerKeysFromJSON(data []byte) (any, error) {
|
||||
var raw any
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lowerKeys(raw), nil
|
||||
}
|
||||
|
||||
func lowerKeys(obj any) any {
|
||||
switch v := obj.(type) {
|
||||
case map[string]any:
|
||||
m := make(map[string]any, len(v))
|
||||
for k, val := range v {
|
||||
m[firstLetterToLower(k)] = lowerKeys(val)
|
||||
}
|
||||
return m
|
||||
case []any:
|
||||
for i := range v {
|
||||
v[i] = lowerKeys(v[i])
|
||||
}
|
||||
return v
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// sendError 写回错误响应
|
||||
// 业务 Error() 构造的 Result[any] 原样透传保留 Code/Msg/Status,普通 error 包成错误响应
|
||||
func (c *Ctx) sendError(err error) {
|
||||
if result, ok := err.(Result[any]); ok {
|
||||
c.send(result)
|
||||
return
|
||||
}
|
||||
c.send(callError(err))
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type enum interface {
|
||||
Names() []string
|
||||
}
|
||||
|
||||
type displayEnum interface {
|
||||
DisplayNames() []string
|
||||
Names() []string
|
||||
}
|
||||
|
||||
var (
|
||||
enumType = reflect.TypeFor[enum]()
|
||||
displayEnumType = reflect.TypeFor[displayEnum]()
|
||||
)
|
||||
|
||||
var (
|
||||
errMethodNotFound = errors.New("method not found")
|
||||
errEmptyFields = errors.New("serviceName and methodName cannot be empty")
|
||||
errDTORequired = errors.New("method requires a DTO but none provided")
|
||||
)
|
||||
@@ -0,0 +1,156 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type Fun struct {
|
||||
methods map[string]methodInfo
|
||||
boxes *sync.Map // 依赖容器:reflect.Type → reflect.Value
|
||||
guards []*any // 全局 Guard
|
||||
serviceGuards map[string][]*any // 服务级 Guard,按服务名
|
||||
}
|
||||
|
||||
var (
|
||||
errorType = reflect.TypeFor[error]()
|
||||
streamType = reflect.TypeFor[*Stream]()
|
||||
)
|
||||
|
||||
var fun *Fun
|
||||
|
||||
// methodInfo 已注册方法的元信息
|
||||
type methodInfo struct {
|
||||
serviceType reflect.Type // 服务值类型(非指针),每请求新建实例
|
||||
methodIndex int // 方法在实例上的反射索引
|
||||
dtoType reflect.Type // DTO 参数类型,无参数时为 nil
|
||||
isStream bool // 返回签名带 *Stream,走 RequestStreamType
|
||||
}
|
||||
|
||||
func New() *Fun {
|
||||
f := &Fun{
|
||||
methods: map[string]methodInfo{},
|
||||
boxes: &sync.Map{},
|
||||
serviceGuards: map[string][]*any{},
|
||||
}
|
||||
if fun == nil {
|
||||
fun = f
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// GetFun 返回默认 Fun 实例,未初始化时自动创建
|
||||
func GetFun() *Fun {
|
||||
if fun == nil {
|
||||
fun = New()
|
||||
}
|
||||
return fun
|
||||
}
|
||||
|
||||
// BindService 注册服务,要求传入指向结构体的指针
|
||||
// 方法签名约束:
|
||||
// - 参数:最多一个,且必须是 struct(作为 DTO)
|
||||
// - 返回值:只支持四种签名——(error)、(T, error)、(stream, error)、(T, stream, error)
|
||||
//
|
||||
// guardList 为该服务绑定的 Guard,方法调用前按注册顺序执行
|
||||
func (f *Fun) BindService(service any, guardList ...Guard) {
|
||||
t := reflect.TypeOf(service)
|
||||
// 必须是指针指向的结构体,匿名类型无法注册
|
||||
if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
|
||||
panic("fun: BindService requires a pointer to a struct")
|
||||
}
|
||||
name := t.Elem().Name()
|
||||
if name == "" {
|
||||
panic("fun: BindService requires a named type")
|
||||
}
|
||||
|
||||
boxWired(service, f)
|
||||
|
||||
serviceGuards := make([]*any, 0, len(guardList))
|
||||
for _, guard := range guardList {
|
||||
checkGuard(guard)
|
||||
serviceGuards = append(serviceGuards, serviceGuardWired(guard, f))
|
||||
}
|
||||
f.serviceGuards[name] = serviceGuards
|
||||
|
||||
for m := range t.Methods() {
|
||||
m := m
|
||||
// Ctx 命名持有 *fasthttp.RequestCtx(非嵌入),服务方法集只含业务方法,无需过滤提升方法
|
||||
mt := m.Type
|
||||
|
||||
// 参数:接收者 + 最多一个 DTO(NumIn() 含接收者),DTO 必须是 struct
|
||||
if mt.NumIn() > 2 {
|
||||
panic(fmt.Sprintf("fun: method %s has more than one parameter", m.Name))
|
||||
}
|
||||
var dtoType reflect.Type
|
||||
if mt.NumIn() == 2 {
|
||||
dtoType = mt.In(1)
|
||||
if dtoType.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("fun: method %s parameter must be a struct", m.Name))
|
||||
}
|
||||
checkType(dtoType)
|
||||
}
|
||||
|
||||
// 返回值只支持四种签名:error / (T, error) / (stream, error) / (T, stream, error)
|
||||
isStream := false
|
||||
switch mt.NumOut() {
|
||||
case 1:
|
||||
// 情况 1:func(...) error
|
||||
if mt.Out(0) != errorType {
|
||||
panic(fmt.Sprintf("fun: method %s must return (error), (T, error), (stream, error) or (T, stream, error)", m.Name))
|
||||
}
|
||||
case 2:
|
||||
// 情况 2:func(...) (T, error) 或 func(...) (*Stream, error)
|
||||
if mt.Out(1) != errorType {
|
||||
panic(fmt.Sprintf("fun: method %s last return value must be error", m.Name))
|
||||
}
|
||||
isStream = mt.Out(0) == streamType
|
||||
case 3:
|
||||
// 情况 3:func(...) (T, *Stream, error)
|
||||
if mt.Out(2) != errorType {
|
||||
panic(fmt.Sprintf("fun: method %s last return value must be error", m.Name))
|
||||
}
|
||||
if mt.Out(1) != streamType {
|
||||
panic(fmt.Sprintf("fun: method %s second return value must be *Stream", m.Name))
|
||||
}
|
||||
isStream = true
|
||||
default:
|
||||
panic(fmt.Sprintf("fun: method %s must return (error), (T, error), (stream, error) or (T, stream, error)", m.Name))
|
||||
}
|
||||
|
||||
// 注册到 "ServiceName.MethodName"
|
||||
f.methods[name+"."+m.Name] = methodInfo{
|
||||
serviceType: t.Elem(),
|
||||
methodIndex: m.Index,
|
||||
dtoType: dtoType,
|
||||
isStream: isStream,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BindGuard 注册全局 Guard,对所有服务生效
|
||||
func (f *Fun) BindGuard(guard Guard) {
|
||||
checkGuard(guard)
|
||||
f.guards = append(f.guards, serviceGuardWired(guard, f))
|
||||
}
|
||||
|
||||
// callGuard 按 全局 → 服务级 顺序执行 Guard
|
||||
func (f *Fun) callGuard(c *Ctx, serviceName string) {
|
||||
for _, g := range f.guards {
|
||||
(*g).(Guard).Guard(*c)
|
||||
}
|
||||
for _, g := range f.serviceGuards[serviceName] {
|
||||
(*g).(Guard).Guard(*c)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fun) Start(port uint16) {
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
err := fasthttp.ListenAndServe(addr, f.handle)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TestRepo struct{}
|
||||
|
||||
type TestDto struct {
|
||||
Name string
|
||||
Age uint8
|
||||
}
|
||||
|
||||
type TestSvc struct {
|
||||
Ctx
|
||||
Repo *TestRepo
|
||||
}
|
||||
|
||||
var guardHit = false
|
||||
|
||||
type TestGuard struct{}
|
||||
|
||||
func (g *TestGuard) Guard(ctx Ctx) {
|
||||
guardHit = true
|
||||
}
|
||||
|
||||
func (s *TestSvc) Hello(dto TestDto) (string, error) {
|
||||
if s.Ip == "" {
|
||||
return "", errors.New("no ip")
|
||||
}
|
||||
if s.Repo == nil {
|
||||
return "", errors.New("no repo")
|
||||
}
|
||||
return "hi " + dto.Name, nil
|
||||
}
|
||||
|
||||
func (s *TestSvc) Count(dto TestDto) (*Stream, error) {
|
||||
st := Stream{}
|
||||
go func() {
|
||||
for i := 0; i < 3; i++ {
|
||||
st.Send(fmt.Sprintf("n%d", i))
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
st.Close()
|
||||
}()
|
||||
return &st, nil
|
||||
}
|
||||
|
||||
func TestCtxBoxInject(t *testing.T) {
|
||||
f := New()
|
||||
guardHit = false
|
||||
f.BindService(&TestSvc{}, &TestGuard{})
|
||||
c := &Ctx{Ip: "1.2.3.4", MethodName: "Hello", ServiceName: "TestSvc"}
|
||||
data := map[string]any{"name": "tom", "age": 1}
|
||||
c.Data = &data
|
||||
|
||||
var streamCh chan any
|
||||
var streamDone chan struct{}
|
||||
res, err := f.invoke(c, &streamCh, &streamDone)
|
||||
if err != nil {
|
||||
t.Fatalf("invoke err: %v", err)
|
||||
}
|
||||
if (*res.Data).(string) != "hi tom" {
|
||||
t.Fatalf("unexpected data: %v", *res.Data)
|
||||
}
|
||||
if !guardHit {
|
||||
t.Fatal("guard not executed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDtoRequired(t *testing.T) {
|
||||
f := New()
|
||||
f.BindService(&TestSvc{})
|
||||
c := &Ctx{Ip: "x", MethodName: "Hello", ServiceName: "TestSvc"}
|
||||
data := map[string]any{"age": 1}
|
||||
c.Data = &data
|
||||
|
||||
var streamCh chan any
|
||||
var streamDone chan struct{}
|
||||
_, err := f.invoke(c, &streamCh, &streamDone)
|
||||
if err == nil {
|
||||
t.Fatal("expected missing-field error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenCode(t *testing.T) {
|
||||
GetFun().BindService(&TestSvc{})
|
||||
SetOutput(t.TempDir())
|
||||
GenCode(GenGo{}, GenTs{})
|
||||
if _, err := os.Stat(filepath.Join(getDirectory(), "go", "test_svc.go")); err != nil {
|
||||
t.Fatalf("go service file missing: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(getDirectory(), "ts", "testSvc.ts")); err != nil {
|
||||
t.Fatalf("ts service file missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func startServer(t *testing.T, port uint16) *Fun {
|
||||
f := New()
|
||||
f.BindService(&TestSvc{})
|
||||
go f.Start(port)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
return f
|
||||
}
|
||||
|
||||
// postCell 以标准库发起 /cell 调用并返回解码后的 Result
|
||||
func postCell(t *testing.T, port uint16, body string) Result[any] {
|
||||
t.Helper()
|
||||
resp, err := http.Post(fmt.Sprintf("http://127.0.0.1:%d/cell", port), "application/json",
|
||||
bytes.NewReader([]byte(body)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var out Result[any]
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestE2ERequest(t *testing.T) {
|
||||
startServer(t, 39001)
|
||||
res := postCell(t, 39001, `{"serviceName":"TestSvc","methodName":"Hello","data":{"name":"tom","age":1}}`)
|
||||
if res.Status != 0 || res.Data == nil || (*res.Data).(string) != "hi tom" {
|
||||
t.Fatalf("unexpected result: %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestE2EStream(t *testing.T) {
|
||||
startServer(t, 39002)
|
||||
resp, err := http.Post("http://127.0.0.1:39002/cell", "application/json",
|
||||
bytes.NewReader([]byte(`{"serviceName":"TestSvc","methodName":"Count","data":{"name":"x","age":1}}`)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var got []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
line := bytes.TrimSpace(scanner.Bytes())
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
got = append(got, string(line))
|
||||
}
|
||||
if len(got) != 3 || got[0] != `"n0"` || got[2] != `"n2"` {
|
||||
t.Fatalf("unexpected stream: %v", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type Gen interface {
|
||||
typeToTemplateType(t reflect.Type) string
|
||||
genService(svc *genSvc, serviceContext *genServiceType)
|
||||
genDefaultService()
|
||||
genStruct(t reflect.Type) *genImportType
|
||||
getEnum(t reflect.Type) *genImportType
|
||||
getName() string
|
||||
}
|
||||
|
||||
// genSvc 生成器视图下的服务
|
||||
type genSvc struct {
|
||||
name string
|
||||
methods []*genMethod
|
||||
}
|
||||
|
||||
// genMethod 生成器视图下的方法
|
||||
type genMethod struct {
|
||||
name string
|
||||
sig reflect.Type // 方法签名类型(不含接收者),Out(0) 为返回类型
|
||||
dtoType reflect.Type
|
||||
isStream bool
|
||||
}
|
||||
|
||||
// serviceGroups 按服务名分组已注册方法
|
||||
func (f *Fun) serviceGroups() map[string][]*genMethod {
|
||||
groups := map[string][]*genMethod{}
|
||||
for key, m := range f.methods {
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
svc, name := parts[0], parts[1]
|
||||
sig := reflect.New(m.serviceType).Method(m.methodIndex).Type()
|
||||
groups[svc] = append(groups[svc], &genMethod{
|
||||
name: name,
|
||||
sig: sig,
|
||||
dtoType: m.dtoType,
|
||||
isStream: m.isStream,
|
||||
})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
type genType struct {
|
||||
GenServiceList []*genServiceType
|
||||
}
|
||||
|
||||
type genMethodType struct {
|
||||
MethodName string
|
||||
ReturnValueText string
|
||||
DtoText string
|
||||
ArgsText string
|
||||
GenericTypeText string
|
||||
IsProxy bool
|
||||
IsStream bool
|
||||
}
|
||||
|
||||
type genEnumType struct {
|
||||
Names []string
|
||||
DisplayNames []string
|
||||
Name string
|
||||
}
|
||||
|
||||
type genImportType struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type genServiceType struct {
|
||||
ServiceName string
|
||||
GenMethodTypeList []*genMethodType
|
||||
GenImport []*genImportType
|
||||
IsIncludeProxy bool
|
||||
IsIncludeStream bool
|
||||
}
|
||||
|
||||
type genClassType struct {
|
||||
Name string
|
||||
GenImport []*genImportType
|
||||
GenClassFieldType []*genClassFieldType
|
||||
}
|
||||
|
||||
type genClassFieldType struct {
|
||||
Name string
|
||||
Type string
|
||||
Tag string
|
||||
}
|
||||
|
||||
func deduplicateServiceImports(imports []*genImportType) []*genImportType {
|
||||
seen := make(map[string]bool)
|
||||
var result []*genImportType
|
||||
for _, imp := range imports {
|
||||
if !seen[imp.Name] {
|
||||
seen[imp.Name] = true
|
||||
result = append(result, imp)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseGenericTypeParams(typeName string) string {
|
||||
start := strings.Index(typeName, "[")
|
||||
end := strings.LastIndex(typeName, "]")
|
||||
paramsStr := typeName[start+1 : end]
|
||||
params := strings.Split(paramsStr, ",")
|
||||
for i, param := range params {
|
||||
LL := strings.Split(strings.TrimSpace(param), ".")
|
||||
params[i] = firstLetterToUpper(LL[len(LL)-1])
|
||||
}
|
||||
return strings.Join(params, "")
|
||||
}
|
||||
|
||||
func getGenericTypeName(typeName string) string {
|
||||
start := strings.Index(typeName, "[")
|
||||
return typeName[0:start]
|
||||
}
|
||||
|
||||
func genCode(templateContent string, outputFileName string, templateData any, languageName string) {
|
||||
tmpl, err := template.New(languageName).Parse(templateContent)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, templateData)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
code := buf.Bytes()
|
||||
fullPath := filepath.Join(getDirectory(), languageName)
|
||||
|
||||
_, err = os.Stat(fullPath)
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(fullPath, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(fullPath, outputFileName+"."+languageName), code, 0644)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// GenCode 执行代码生成:清空输出目录后按顺序运行每个生成器
|
||||
func GenCode(genList ...Gen) {
|
||||
if err := os.RemoveAll(getDirectory()); err != nil && !os.IsNotExist(err) {
|
||||
panic(err.Error())
|
||||
}
|
||||
for _, gen := range genList {
|
||||
gen.genDefaultService()
|
||||
}
|
||||
}
|
||||
|
||||
var directory = "./gen"
|
||||
|
||||
func SetOutput(path string) {
|
||||
directory = path
|
||||
}
|
||||
|
||||
func getDirectory() string {
|
||||
return directory
|
||||
}
|
||||
|
||||
func camelToSnake(s string) string {
|
||||
re := regexp.MustCompile(`([a-z0-9])([A-Z])`)
|
||||
snake := re.ReplaceAllString(s, `${1}_${2}`)
|
||||
return strings.ToLower(snake)
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GenGo struct {
|
||||
template templateGo
|
||||
}
|
||||
|
||||
func (ctx GenGo) typeToTemplateType(t reflect.Type) string {
|
||||
text := ""
|
||||
if t.Kind() == reflect.Ptr {
|
||||
text += "*"
|
||||
t = t.Elem()
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Slice:
|
||||
text += "[]" + ctx.typeToTemplateType(t.Elem())
|
||||
default:
|
||||
text += t.Name()
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (ctx GenGo) genService(svc *genSvc, serviceContext *genServiceType) {
|
||||
for _, gm := range svc.methods {
|
||||
var returnValueText string
|
||||
var dtoText string
|
||||
var argsText string
|
||||
var genericTypeText string
|
||||
|
||||
// () error:无数据返回,客户端类型为 Void
|
||||
if gm.sig.NumOut() == 1 && gm.sig.Out(0) == errorType {
|
||||
genericTypeText = "Void"
|
||||
returnValueText = "Result[Void]"
|
||||
if gm.dtoType != nil {
|
||||
v := ctx.typeToTemplateType(gm.dtoType)
|
||||
if !strings.Contains(v, "[]") && strings.Contains(v, "[") {
|
||||
dtoText += "dto " + getGenericTypeName(v) + parseGenericTypeParams(v)
|
||||
} else {
|
||||
dtoText += "dto " + v
|
||||
}
|
||||
argsText += ",dto"
|
||||
ctx.genStruct(gm.dtoType)
|
||||
}
|
||||
serviceContext.GenMethodTypeList = append(serviceContext.GenMethodTypeList, &genMethodType{
|
||||
MethodName: gm.name,
|
||||
ReturnValueText: returnValueText,
|
||||
DtoText: dtoText,
|
||||
ArgsText: argsText,
|
||||
GenericTypeText: genericTypeText,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
returnType := gm.sig.Out(0)
|
||||
if gm.isStream {
|
||||
serviceContext.IsIncludeStream = true
|
||||
if returnType == streamType {
|
||||
genericTypeText = "any"
|
||||
returnValueText = "Void"
|
||||
} else {
|
||||
t := ctx.typeToTemplateType(returnType)
|
||||
if !strings.Contains(t, "[]") && strings.Contains(t, "[") {
|
||||
genericTypeText = getGenericTypeName(t) + parseGenericTypeParams(t)
|
||||
} else {
|
||||
genericTypeText = t
|
||||
}
|
||||
returnValueText = genericTypeText
|
||||
ctx.genReturnTypes(returnType)
|
||||
}
|
||||
} else {
|
||||
t := ctx.typeToTemplateType(returnType)
|
||||
if !strings.Contains(t, "[]") && strings.Contains(t, "[") {
|
||||
returnValueText = getGenericTypeName(t) + parseGenericTypeParams(t)
|
||||
} else {
|
||||
returnValueText = t
|
||||
}
|
||||
genericTypeText = returnValueText
|
||||
ctx.genReturnTypes(returnType)
|
||||
returnValueText = "Result[" + returnValueText + "]"
|
||||
}
|
||||
|
||||
if gm.dtoType != nil {
|
||||
v := ctx.typeToTemplateType(gm.dtoType)
|
||||
if !strings.Contains(v, "[]") && strings.Contains(v, "[") {
|
||||
dtoText += "dto " + getGenericTypeName(v) + parseGenericTypeParams(v)
|
||||
} else {
|
||||
dtoText += "dto " + v
|
||||
}
|
||||
argsText += ",dto"
|
||||
ctx.genStruct(gm.dtoType)
|
||||
}
|
||||
|
||||
serviceContext.GenMethodTypeList = append(serviceContext.GenMethodTypeList, &genMethodType{
|
||||
MethodName: gm.name,
|
||||
ReturnValueText: returnValueText,
|
||||
DtoText: dtoText,
|
||||
ArgsText: argsText,
|
||||
GenericTypeText: genericTypeText,
|
||||
IsStream: gm.isStream,
|
||||
})
|
||||
}
|
||||
genCode(ctx.template.genServiceTemplate(), camelToSnake(svc.name), serviceContext, ctx.getName())
|
||||
}
|
||||
|
||||
// genReturnTypes 递归生成返回类型涉及的 struct/enum 定义
|
||||
func (ctx GenGo) genReturnTypes(returnType reflect.Type) {
|
||||
if returnType.Kind() == reflect.Ptr {
|
||||
returnType = returnType.Elem()
|
||||
}
|
||||
if returnType.Kind() == reflect.Struct {
|
||||
ctx.genStruct(returnType)
|
||||
}
|
||||
if returnType.Kind() == reflect.Slice {
|
||||
fieldType := returnType.Elem()
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
if fieldType.Kind() == reflect.Struct {
|
||||
ctx.genStruct(fieldType)
|
||||
}
|
||||
}
|
||||
if returnType.Kind() == reflect.Uint8 && (returnType.Implements(displayEnumType) || returnType.Implements(enumType)) {
|
||||
ctx.getEnum(returnType)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx GenGo) genDefaultService() {
|
||||
f := GetFun()
|
||||
genContext := genType{GenServiceList: []*genServiceType{}}
|
||||
|
||||
for svcName, methods := range f.serviceGroups() {
|
||||
serviceContext := &genServiceType{
|
||||
ServiceName: svcName,
|
||||
GenMethodTypeList: []*genMethodType{},
|
||||
}
|
||||
genContext.GenServiceList = append(genContext.GenServiceList, serviceContext)
|
||||
ctx.genService(&genSvc{name: svcName, methods: methods}, serviceContext)
|
||||
}
|
||||
genCode(ctx.template.genDefaultServiceTemplate(), "fun", genContext, ctx.getName())
|
||||
}
|
||||
|
||||
func (ctx GenGo) genStruct(t reflect.Type) *genImportType {
|
||||
var structTemplate genClassType
|
||||
if !strings.Contains(t.String(), "[]") && strings.Contains(t.String(), "[") {
|
||||
structTemplate = genClassType{
|
||||
Name: getGenericTypeName(t.Name()) + parseGenericTypeParams(t.Name()),
|
||||
}
|
||||
} else {
|
||||
structTemplate = genClassType{
|
||||
Name: t.Name(),
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
fieldType := field.Type
|
||||
jsType := ctx.typeToTemplateType(fieldType)
|
||||
name := field.Name
|
||||
tag := "`json:\"" + firstLetterToLower(name) + "\"`"
|
||||
if !strings.Contains(jsType, "[]") && strings.Contains(jsType, "[") {
|
||||
structTemplate.GenClassFieldType = append(structTemplate.GenClassFieldType, &genClassFieldType{
|
||||
Name: name,
|
||||
Type: getGenericTypeName(jsType) + parseGenericTypeParams(jsType),
|
||||
Tag: tag,
|
||||
})
|
||||
} else {
|
||||
structTemplate.GenClassFieldType = append(structTemplate.GenClassFieldType, &genClassFieldType{
|
||||
Name: name,
|
||||
Type: jsType,
|
||||
Tag: tag,
|
||||
})
|
||||
}
|
||||
|
||||
if fieldType.Kind() == reflect.Struct {
|
||||
ctx.genStruct(fieldType)
|
||||
}
|
||||
if fieldType.Kind() == reflect.Slice && fieldType.Elem().Kind() == reflect.Struct {
|
||||
ctx.genStruct(fieldType.Elem())
|
||||
}
|
||||
if fieldType.Kind() == reflect.Uint8 && (fieldType.Implements(displayEnumType) || fieldType.Implements(enumType)) {
|
||||
ctx.getEnum(fieldType)
|
||||
}
|
||||
}
|
||||
|
||||
genCode(
|
||||
ctx.template.genStructTemplate(),
|
||||
camelToSnake(structTemplate.Name),
|
||||
structTemplate,
|
||||
ctx.getName(),
|
||||
)
|
||||
return &genImportType{}
|
||||
}
|
||||
|
||||
func (ctx GenGo) getEnum(t reflect.Type) *genImportType {
|
||||
var enumTemplate genEnumType
|
||||
statusValue := reflect.New(t).Elem()
|
||||
if t.Implements(displayEnumType) {
|
||||
enumValue := statusValue.Interface().(displayEnum)
|
||||
enumTemplate.Names = enumValue.Names()
|
||||
enumTemplate.DisplayNames = enumValue.DisplayNames()
|
||||
} else {
|
||||
enumValue := statusValue.Interface().(enum)
|
||||
enumTemplate.Names = enumValue.Names()
|
||||
}
|
||||
enumTemplate.Name = t.Name()
|
||||
|
||||
genCode(
|
||||
ctx.template.genEnumTemplate(),
|
||||
camelToSnake(t.Name()),
|
||||
enumTemplate,
|
||||
ctx.getName(),
|
||||
)
|
||||
return &genImportType{}
|
||||
}
|
||||
|
||||
func (ctx GenGo) getName() string {
|
||||
return "go"
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GenTs struct {
|
||||
template templateTs
|
||||
}
|
||||
|
||||
func (ctx GenTs) typeToTemplateType(t reflect.Type) string {
|
||||
text := ""
|
||||
if t.Kind() == reflect.Ptr {
|
||||
text += " | null"
|
||||
t = t.Elem()
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if t.Kind() == reflect.Uint8 && (t.Implements(displayEnumType) || t.Implements(enumType)) {
|
||||
text = t.Name() + text
|
||||
} else {
|
||||
text = "number" + text
|
||||
}
|
||||
case reflect.Bool:
|
||||
text = "boolean" + text
|
||||
case reflect.String, reflect.Struct:
|
||||
text = t.Name() + text
|
||||
default:
|
||||
text = ctx.typeToTemplateType(t.Elem()) + "[]" + text
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (ctx GenTs) genService(svc *genSvc, serviceContext *genServiceType) {
|
||||
var nestedImports []*genImportType
|
||||
|
||||
for _, gm := range svc.methods {
|
||||
var returnValueText string
|
||||
var dtoText string
|
||||
var argsText string
|
||||
var genericTypeText string
|
||||
|
||||
// () error:无数据返回,客户端类型为 void
|
||||
if gm.sig.NumOut() == 1 && gm.sig.Out(0) == errorType {
|
||||
genericTypeText = "void"
|
||||
returnValueText = "result<void>"
|
||||
if gm.dtoType != nil {
|
||||
v := firstLetterToLower(ctx.typeToTemplateType(gm.dtoType))
|
||||
if !strings.Contains(v, "[]") && strings.Contains(v, "[") {
|
||||
dtoText += "dto:" + getGenericTypeName(v) + parseGenericTypeParams(v)
|
||||
} else {
|
||||
dtoText += "dto:" + v
|
||||
}
|
||||
argsText += ",dto"
|
||||
nestedImports = append(nestedImports, ctx.genStruct(gm.dtoType))
|
||||
}
|
||||
serviceContext.GenMethodTypeList = append(serviceContext.GenMethodTypeList, &genMethodType{
|
||||
MethodName: firstLetterToLower(gm.name),
|
||||
ReturnValueText: returnValueText,
|
||||
DtoText: dtoText,
|
||||
ArgsText: argsText,
|
||||
GenericTypeText: genericTypeText,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
returnType := gm.sig.Out(0)
|
||||
if gm.isStream {
|
||||
serviceContext.IsIncludeStream = true
|
||||
if returnType == streamType {
|
||||
genericTypeText = "any"
|
||||
returnValueText = "void"
|
||||
} else {
|
||||
t := firstLetterToLower(ctx.typeToTemplateType(returnType))
|
||||
if !strings.Contains(t, "[]") && strings.Contains(t, "[") {
|
||||
genericTypeText = getGenericTypeName(t) + parseGenericTypeParams(t)
|
||||
} else {
|
||||
genericTypeText = t
|
||||
}
|
||||
returnValueText = "void"
|
||||
nestedImports = ctx.genReturnTypes(returnType, nestedImports)
|
||||
}
|
||||
} else {
|
||||
t := firstLetterToLower(ctx.typeToTemplateType(returnType))
|
||||
if !strings.Contains(t, "[]") && strings.Contains(t, "[") {
|
||||
returnValueText = getGenericTypeName(t) + parseGenericTypeParams(t)
|
||||
} else {
|
||||
returnValueText = t
|
||||
}
|
||||
genericTypeText = returnValueText
|
||||
nestedImports = ctx.genReturnTypes(returnType, nestedImports)
|
||||
returnValueText = "result<" + returnValueText + ">"
|
||||
}
|
||||
|
||||
if gm.dtoType != nil {
|
||||
v := firstLetterToLower(ctx.typeToTemplateType(gm.dtoType))
|
||||
if !strings.Contains(v, "[]") && strings.Contains(v, "[") {
|
||||
dtoText += "dto:" + getGenericTypeName(v) + parseGenericTypeParams(v)
|
||||
} else {
|
||||
dtoText += "dto:" + v
|
||||
}
|
||||
argsText += ",dto"
|
||||
nestedImports = append(nestedImports, ctx.genStruct(gm.dtoType))
|
||||
}
|
||||
|
||||
serviceContext.GenMethodTypeList = append(serviceContext.GenMethodTypeList, &genMethodType{
|
||||
MethodName: firstLetterToLower(gm.name),
|
||||
ReturnValueText: returnValueText,
|
||||
DtoText: dtoText,
|
||||
ArgsText: argsText,
|
||||
GenericTypeText: firstLetterToLower(genericTypeText),
|
||||
IsStream: gm.isStream,
|
||||
})
|
||||
}
|
||||
serviceContext.GenImport = deduplicateServiceImports(nestedImports)
|
||||
|
||||
genCode(
|
||||
ctx.template.genServiceTemplate(),
|
||||
firstLetterToLower(svc.name),
|
||||
serviceContext,
|
||||
ctx.getName(),
|
||||
)
|
||||
}
|
||||
|
||||
// genReturnTypes 递归生成返回类型涉及的 struct/enum 导入
|
||||
func (ctx GenTs) genReturnTypes(returnType reflect.Type, nestedImports []*genImportType) []*genImportType {
|
||||
if returnType.Kind() == reflect.Ptr {
|
||||
returnType = returnType.Elem()
|
||||
}
|
||||
if returnType.Kind() == reflect.Struct {
|
||||
nestedImports = append(nestedImports, ctx.genStruct(returnType))
|
||||
}
|
||||
if returnType.Kind() == reflect.Slice {
|
||||
fieldType := returnType.Elem()
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
if fieldType.Kind() == reflect.Struct {
|
||||
nestedImports = append(nestedImports, ctx.genStruct(fieldType))
|
||||
}
|
||||
}
|
||||
if returnType.Kind() == reflect.Uint8 && (returnType.Implements(displayEnumType) || returnType.Implements(enumType)) {
|
||||
nestedImports = append(nestedImports, ctx.getEnum(returnType))
|
||||
}
|
||||
return nestedImports
|
||||
}
|
||||
|
||||
func (ctx GenTs) genDefaultService() {
|
||||
f := GetFun()
|
||||
genContext := genType{GenServiceList: []*genServiceType{}}
|
||||
|
||||
for svcName, methods := range f.serviceGroups() {
|
||||
serviceContext := &genServiceType{
|
||||
ServiceName: firstLetterToLower(svcName),
|
||||
GenMethodTypeList: []*genMethodType{},
|
||||
}
|
||||
genContext.GenServiceList = append(genContext.GenServiceList, serviceContext)
|
||||
ctx.genService(&genSvc{name: svcName, methods: methods}, serviceContext)
|
||||
}
|
||||
genCode(ctx.template.genClientTemplate(), "client", nil, ctx.getName())
|
||||
genCode(ctx.template.genDefaultServiceTemplate(), "fun", genContext, ctx.getName())
|
||||
}
|
||||
|
||||
func (ctx GenTs) genStruct(t reflect.Type) *genImportType {
|
||||
var structTemplate genClassType
|
||||
if !strings.Contains(t.String(), "[]") && strings.Contains(t.String(), "[") {
|
||||
structTemplate = genClassType{
|
||||
Name: firstLetterToLower(getGenericTypeName(t.Name())) + parseGenericTypeParams(t.Name()),
|
||||
}
|
||||
} else {
|
||||
structTemplate = genClassType{
|
||||
Name: firstLetterToLower(t.Name()),
|
||||
}
|
||||
}
|
||||
var nestedImports []*genImportType
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
fieldType := field.Type
|
||||
jsType := ctx.typeToTemplateType(fieldType)
|
||||
name := field.Name
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
name += "?"
|
||||
}
|
||||
if !strings.Contains(jsType, "[]") && strings.Contains(jsType, "[") {
|
||||
structTemplate.GenClassFieldType = append(structTemplate.GenClassFieldType, &genClassFieldType{
|
||||
Name: firstLetterToLower(name),
|
||||
Type: firstLetterToLower(getGenericTypeName(jsType)) + parseGenericTypeParams(jsType),
|
||||
})
|
||||
} else {
|
||||
structTemplate.GenClassFieldType = append(structTemplate.GenClassFieldType, &genClassFieldType{
|
||||
Name: firstLetterToLower(name),
|
||||
Type: firstLetterToLower(jsType),
|
||||
})
|
||||
}
|
||||
|
||||
if fieldType.Kind() == reflect.Struct {
|
||||
nestedImports = append(nestedImports, ctx.genStruct(fieldType))
|
||||
}
|
||||
if fieldType.Kind() == reflect.Slice && fieldType.Elem().Kind() == reflect.Struct {
|
||||
nestedImports = append(nestedImports, ctx.genStruct(fieldType.Elem()))
|
||||
}
|
||||
if fieldType.Kind() == reflect.Uint8 && (fieldType.Implements(displayEnumType) || fieldType.Implements(enumType)) {
|
||||
nestedImports = append(nestedImports, ctx.getEnum(fieldType))
|
||||
}
|
||||
}
|
||||
|
||||
structTemplate.GenImport = deduplicateServiceImports(nestedImports)
|
||||
|
||||
genCode(
|
||||
ctx.template.genStructTemplate(),
|
||||
structTemplate.Name,
|
||||
structTemplate,
|
||||
ctx.getName(),
|
||||
)
|
||||
|
||||
if !strings.Contains(t.String(), "[]") && strings.Contains(t.String(), "[") {
|
||||
return &genImportType{Name: structTemplate.Name}
|
||||
}
|
||||
return &genImportType{Name: firstLetterToLower(t.Name())}
|
||||
}
|
||||
|
||||
func (ctx GenTs) getEnum(t reflect.Type) *genImportType {
|
||||
var enumTemplate genEnumType
|
||||
statusValue := reflect.New(t).Elem()
|
||||
if t.Implements(displayEnumType) {
|
||||
enumValue := statusValue.Interface().(displayEnum)
|
||||
enumTemplate.Names = enumValue.Names()
|
||||
enumTemplate.DisplayNames = enumValue.DisplayNames()
|
||||
} else {
|
||||
enumValue := statusValue.Interface().(enum)
|
||||
enumTemplate.Names = enumValue.Names()
|
||||
}
|
||||
enumTemplate.Name = firstLetterToLower(t.Name())
|
||||
|
||||
genCode(
|
||||
ctx.template.genEnumTemplate(),
|
||||
firstLetterToLower(t.Name()),
|
||||
enumTemplate,
|
||||
ctx.getName(),
|
||||
)
|
||||
return &genImportType{Name: firstLetterToLower(t.Name())}
|
||||
}
|
||||
|
||||
func (ctx GenTs) getName() string {
|
||||
return "ts"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
module github.com/cyi-cc/fun
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require github.com/valyala/fasthttp v1.73.0
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.2 // indirect
|
||||
github.com/klauspost/compress v1.19.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
|
||||
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s=
|
||||
github.com/valyala/fasthttp v1.73.0/go.mod h1:EtXQDHaR+5P18p8wqDRFpUhxr108Ga9mXvVJXHRrN2k=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
@@ -0,0 +1,226 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// handle 处理 HTTP 请求
|
||||
func (f *Fun) handle(fastCtx *fasthttp.RequestCtx) {
|
||||
ctx := &Ctx{RequestCtx: fastCtx}
|
||||
defer f.handlePanic(ctx)
|
||||
|
||||
if ctx.path() != "/cell" {
|
||||
ctx.setStatusCode(fasthttp.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if !ctx.isPost() {
|
||||
ctx.setStatusCode(fasthttp.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
body := ctx.postBody()
|
||||
var requestInfo RequestInfo[map[string]any]
|
||||
if err := json.Unmarshal(body, &requestInfo); err != nil {
|
||||
ctx.sendError(err)
|
||||
return
|
||||
}
|
||||
requestInfo.MethodName = firstLetterToUpper(requestInfo.MethodName)
|
||||
requestInfo.ServiceName = firstLetterToUpper(requestInfo.ServiceName)
|
||||
if requestInfo.MethodName == "" || requestInfo.ServiceName == "" {
|
||||
ctx.sendError(errEmptyFields)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Ip = ctx.remoteIP().String()
|
||||
ctx.State = requestInfo.State
|
||||
ctx.MethodName = requestInfo.MethodName
|
||||
ctx.ServiceName = requestInfo.ServiceName
|
||||
ctx.Data = requestInfo.Data
|
||||
|
||||
// 流式方法:响应保持打开,以 NDJSON 行推送(Streamable HTTP)
|
||||
// streamCh != nil 表示流式方法;业务返回的 *Stream 在 invoke 内完成注入
|
||||
var streamCh chan any
|
||||
var streamDone chan struct{}
|
||||
|
||||
result, err := f.invoke(ctx, &streamCh, &streamDone)
|
||||
if err != nil {
|
||||
ctx.sendError(err)
|
||||
return
|
||||
}
|
||||
if streamCh != nil {
|
||||
fastCtx.Response.Header.SetContentType("application/x-ndjson")
|
||||
fastCtx.Response.Header.Set("Cache-Control", "no-cache")
|
||||
fastCtx.Response.Header.Set("Connection", "keep-alive")
|
||||
fastCtx.SetBodyStreamWriter(func(w *bufio.Writer) {
|
||||
writeLine := func(v any) bool {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
raw, err := lowerKeysFromJSON(data)
|
||||
if err == nil {
|
||||
if data, err = json.Marshal(raw); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "%s\n", data); err != nil {
|
||||
return false
|
||||
}
|
||||
if err := w.Flush(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
// (T, stream, error):T 作为流的第一条消息下发
|
||||
if result.Data != nil {
|
||||
if !writeLine(*result.Data) {
|
||||
close(streamDone)
|
||||
return
|
||||
}
|
||||
}
|
||||
for message := range streamCh {
|
||||
if !writeLine(message) {
|
||||
close(streamDone)
|
||||
return
|
||||
}
|
||||
}
|
||||
close(streamDone)
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.send(*result)
|
||||
}
|
||||
|
||||
// handlePanic 兜底处理 panic:归一为 error 后写回错误响应,并记录完整堆栈日志
|
||||
func (f *Fun) handlePanic(c *Ctx) {
|
||||
if v := recover(); v != nil {
|
||||
var err error
|
||||
if e, ok := v.(error); ok {
|
||||
err = e
|
||||
} else {
|
||||
err = fmt.Errorf("panic (%s.%s): %v", c.ServiceName, c.MethodName, v)
|
||||
}
|
||||
ErrorLogger(err.Error(), "\n"+string(debug.Stack()))
|
||||
c.sendError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// invoke 按 "Service.Method" 查找并调用,返回成功结果
|
||||
// 流式方法时,业务返回的 *Stream 完成通道注入(streamCh/streamDone 被创建并填充)
|
||||
// 预期错误(方法不存在、参数缺失、业务失败)以 error 返回,不 panic
|
||||
func (f *Fun) invoke(c *Ctx, streamCh *chan any, streamDone *chan struct{}) (*Result[any], error) {
|
||||
key := c.ServiceName + "." + c.MethodName
|
||||
method, ok := f.methods[key]
|
||||
if !ok {
|
||||
return nil, errMethodNotFound
|
||||
}
|
||||
|
||||
f.callGuard(c, c.ServiceName)
|
||||
|
||||
var args []reflect.Value
|
||||
if method.dtoType != nil {
|
||||
if c.Data == nil {
|
||||
return nil, errDTORequired
|
||||
}
|
||||
if err := checkDto(method.dtoType, *c.Data, c.MethodName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := reflect.New(method.dtoType).Elem()
|
||||
if err := convert(c.Data, dto.Addr().Interface()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, dto)
|
||||
}
|
||||
|
||||
// 每请求创建新实例并注入 Ctx/Box 依赖,避免并发共享实例
|
||||
instance := reflect.New(method.serviceType)
|
||||
f.serviceWired(instance.Elem(), c)
|
||||
values := instance.Method(method.methodIndex).Call(args)
|
||||
return callResult(c, values, method, streamCh, streamDone)
|
||||
}
|
||||
|
||||
// callResult 将反射调用结果归一为 Result
|
||||
// 兼容四种签名:(error)、(T, error)、(stream, error)、(T, stream, error)
|
||||
// - 末位返回值是 error 且非 nil → 业务失败,返回 error
|
||||
// - 带 *Stream 的签名:注入推送通道后,仅 (T, stream, error) 返回 T 作为数据
|
||||
func callResult(c *Ctx, values []reflect.Value, method methodInfo, streamCh *chan any, streamDone *chan struct{}) (*Result[any], error) {
|
||||
if last := values[len(values)-1]; last.Type().Implements(errorType) {
|
||||
if !last.IsNil() {
|
||||
// 业务出错但可能已启动 goroutine 调 Send/Close:
|
||||
// 注入一个已取消的流,让它们立即解除阻塞退出,避免 goroutine 泄漏
|
||||
if method.isStream {
|
||||
injectCancelledStream(values, method, streamCh, streamDone)
|
||||
}
|
||||
// 业务 Error() 构造的 Result[any] 作为 error 返回,sendError 里原样透传
|
||||
var result Result[any]
|
||||
if errors.As(last.Interface().(error), &result) {
|
||||
return nil, result
|
||||
}
|
||||
return nil, last.Interface().(error)
|
||||
}
|
||||
values = values[:len(values)-1]
|
||||
}
|
||||
|
||||
if method.isStream {
|
||||
// (stream, error):流在第 0 位;(T, stream, error):流在第 1 位
|
||||
streamIdx := 0
|
||||
if len(values) == 2 {
|
||||
streamIdx = 1
|
||||
}
|
||||
s := values[streamIdx].Interface().(*Stream)
|
||||
if s == nil {
|
||||
return nil, errors.New("fun: method returned nil stream")
|
||||
}
|
||||
*streamCh = make(chan any)
|
||||
*streamDone = make(chan struct{})
|
||||
s.Inject(*streamCh, *streamDone)
|
||||
// (T, stream, error):返回 T;纯流:不返回数据
|
||||
if len(values) == 2 {
|
||||
r := success(values[0].Interface())
|
||||
return &r, nil
|
||||
}
|
||||
r := success(nil)
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
// () error:无数据返回
|
||||
r := success(nil)
|
||||
return &r, nil
|
||||
}
|
||||
r := success(values[0].Interface())
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// injectCancelledStream 业务出错时注入已取消的流通道,
|
||||
// 使正在 Send/Close 上阻塞的业务 goroutine 立即解除并退出
|
||||
func injectCancelledStream(values []reflect.Value, method methodInfo, streamCh *chan any, streamDone *chan struct{}) {
|
||||
streamIdx := 0
|
||||
if len(values) == 3 { // (T, stream, error)
|
||||
streamIdx = 1
|
||||
}
|
||||
s, ok := values[streamIdx].Interface().(*Stream)
|
||||
if !ok || s == nil {
|
||||
return
|
||||
}
|
||||
*streamCh = make(chan any)
|
||||
*streamDone = make(chan struct{})
|
||||
s.Inject(*streamCh, *streamDone)
|
||||
close(*streamDone)
|
||||
}
|
||||
|
||||
// convert 将数据转为 JSON 再反序列化到目标类型,避免手写字段映射
|
||||
func convert(from any, to any) error {
|
||||
data, err := json.Marshal(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, to)
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
PanicLevel uint8 = iota
|
||||
ErrorLevel
|
||||
WarnLevel
|
||||
InfoLevel
|
||||
DebugLevel
|
||||
TraceLevel
|
||||
)
|
||||
|
||||
var logChan = make(chan string, 100)
|
||||
var logWg sync.WaitGroup
|
||||
|
||||
const (
|
||||
TerminalMode uint8 = iota
|
||||
FileMode
|
||||
)
|
||||
|
||||
var logMutex sync.Mutex
|
||||
|
||||
type Logger struct {
|
||||
Level uint8
|
||||
Mode uint8
|
||||
MaxSizeFile uint8 //文件最大大小(MB)
|
||||
MaxNumberFiles uint64 //文件最多数量
|
||||
ExpireLogsDays uint8 //文件保留时间
|
||||
LogFilePath string
|
||||
}
|
||||
|
||||
var logger Logger = Logger{
|
||||
Level: TraceLevel,
|
||||
Mode: TerminalMode,
|
||||
MaxSizeFile: 0,
|
||||
MaxNumberFiles: 0,
|
||||
ExpireLogsDays: 0,
|
||||
LogFilePath: "../log",
|
||||
}
|
||||
|
||||
func init() {
|
||||
go deleteLogWorker()
|
||||
go logWriterWorker()
|
||||
}
|
||||
|
||||
func logWriterWorker() {
|
||||
for text := range logChan {
|
||||
logMutex.Lock()
|
||||
if logger.Mode == FileMode {
|
||||
fileLogger(text)
|
||||
} else {
|
||||
fmt.Println(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteLogWorker() {
|
||||
cleanupExpiredLogs()
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if logger.Mode == FileMode {
|
||||
cleanupExpiredLogs()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getLogFilePath() string {
|
||||
if logger.LogFilePath == "" {
|
||||
return "./log"
|
||||
}
|
||||
return logger.LogFilePath
|
||||
}
|
||||
|
||||
func cleanupExpiredLogs() {
|
||||
if logger.ExpireLogsDays <= 0 {
|
||||
return
|
||||
}
|
||||
_, err := os.Stat(getLogFilePath())
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
entries, err := os.ReadDir(getLogFilePath())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
expireDuration := time.Duration(logger.ExpireLogsDays) * 24 * time.Hour
|
||||
currentTimeMillis := time.Now().UnixMilli()
|
||||
expireThreshold := currentTimeMillis - expireDuration.Milliseconds()
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
fileNameInfo := getFileNameInfo(entry.Name())
|
||||
if fileNameInfo.LoggerTime == 0 {
|
||||
continue
|
||||
}
|
||||
if fileNameInfo.LoggerTime < expireThreshold {
|
||||
fullPath := filepath.Join(getLogFilePath(), entry.Name())
|
||||
err := os.Remove(fullPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getFileNameInfo(name string) fileName {
|
||||
fileNameParts := strings.Split(name, ".log.")
|
||||
if len(fileNameParts) != 2 {
|
||||
deleteLog(name)
|
||||
return fileName{}
|
||||
}
|
||||
dateLayout := "2006-01-02"
|
||||
dateString := fileNameParts[0]
|
||||
fileDate, err := time.Parse(dateLayout, dateString)
|
||||
if err != nil {
|
||||
deleteLog(name)
|
||||
return fileName{}
|
||||
}
|
||||
indexString := fileNameParts[1]
|
||||
indexString = strings.TrimSuffix(indexString, ".log")
|
||||
fileIndex, err := strconv.ParseInt(indexString, 10, 32)
|
||||
if err != nil {
|
||||
deleteLog(name)
|
||||
return fileName{}
|
||||
}
|
||||
return fileName{
|
||||
index: int32(fileIndex),
|
||||
LoggerTime: fileDate.UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
type fileName struct {
|
||||
LoggerTime int64
|
||||
index int32
|
||||
}
|
||||
|
||||
func deleteLog(name string) {
|
||||
fullPath := filepath.Join(getLogFilePath(), name)
|
||||
err := os.Remove(fullPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func fileLogger(text string) {
|
||||
_, err := os.Stat(getLogFilePath())
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(getLogFilePath(), os.ModePerm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
currentDate := getCurrentData()
|
||||
logFileName := currentDate + ".log"
|
||||
logFilePath := filepath.Join(getLogFilePath(), logFileName)
|
||||
logFilePath, err = getNextLogFile(getLogFilePath(), currentDate, text)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
_ = file.Close()
|
||||
}(file)
|
||||
_, _ = file.WriteString(text + "\n")
|
||||
}
|
||||
|
||||
func removeOldestLogFile(entries []os.DirEntry) {
|
||||
if logger.MaxNumberFiles == 0 {
|
||||
return
|
||||
}
|
||||
if uint64(len(entries)) < logger.MaxNumberFiles {
|
||||
return
|
||||
}
|
||||
var newEntries []fileName
|
||||
for _, v := range entries {
|
||||
fileNameInfo := getFileNameInfo(v.Name())
|
||||
if fileNameInfo.LoggerTime != 0 {
|
||||
newEntries = append(newEntries, fileNameInfo)
|
||||
}
|
||||
}
|
||||
if uint64(len(newEntries)) < logger.MaxNumberFiles {
|
||||
return
|
||||
}
|
||||
delNum := uint64(len(newEntries)) - logger.MaxNumberFiles + 1
|
||||
sort.Slice(newEntries, func(i, j int) bool {
|
||||
if newEntries[i].LoggerTime != newEntries[j].LoggerTime {
|
||||
return newEntries[i].LoggerTime < newEntries[j].LoggerTime
|
||||
}
|
||||
return newEntries[i].index < newEntries[j].index
|
||||
})
|
||||
for i := 0; i < int(delNum); i++ {
|
||||
fileName := newEntries[i]
|
||||
t := time.Unix(0, fileName.LoggerTime*int64(time.Millisecond))
|
||||
fileNamePath := filepath.Join(getLogFilePath(), t.Format("2006-01-02")+".log."+strconv.Itoa(int(fileName.index)))
|
||||
deleteLog(fileNamePath)
|
||||
}
|
||||
}
|
||||
|
||||
// getNextLogFile 获取下一个应该写入的日志文件
|
||||
func getNextLogFile(dirPath, dateStr string, text string) (string, error) {
|
||||
entries, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return filepath.Join(dirPath, dateStr+".log.1"), err
|
||||
}
|
||||
var maxIndex int32 = 0
|
||||
var existingFiles []string
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasPrefix(entry.Name(), dateStr+".log") {
|
||||
existingFiles = append(existingFiles, entry.Name())
|
||||
}
|
||||
}
|
||||
if len(existingFiles) == 0 {
|
||||
removeOldestLogFile(entries)
|
||||
return filepath.Join(dirPath, dateStr+".log.1"), nil
|
||||
}
|
||||
for _, fileName := range existingFiles {
|
||||
fileNameInfo := getFileNameInfo(fileName)
|
||||
if fileNameInfo.LoggerTime != 0 && fileNameInfo.index > maxIndex {
|
||||
maxIndex = fileNameInfo.index
|
||||
}
|
||||
}
|
||||
if maxIndex == 0 {
|
||||
removeOldestLogFile(entries)
|
||||
return filepath.Join(dirPath, dateStr+".log.1"), nil
|
||||
}
|
||||
if logger.MaxSizeFile > 0 && maxIndex > 0 {
|
||||
currentFile := filepath.Join(dirPath, fmt.Sprintf("%s.log.%d", dateStr, maxIndex))
|
||||
if fileInfo, err := os.Stat(currentFile); err == nil {
|
||||
maxSizeBytes := int64(logger.MaxSizeFile) * 1024 * 1024
|
||||
if fileInfo.Size()+int64(len(text)) > maxSizeBytes {
|
||||
removeOldestLogFile(entries)
|
||||
return filepath.Join(dirPath, fmt.Sprintf("%s.log.%d", dateStr, maxIndex+1)), nil
|
||||
}
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return filepath.Join(dirPath, fmt.Sprintf("%s.log.%d", dateStr, maxIndex)), nil
|
||||
}
|
||||
|
||||
func ConfigLogger(log Logger) {
|
||||
logger = log
|
||||
}
|
||||
|
||||
func getCurrentTime() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func getCurrentData() string {
|
||||
return time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
func getMethodNameLogger() string {
|
||||
pc, _, _, _ := runtime.Caller(3)
|
||||
fn := runtime.FuncForPC(pc)
|
||||
charsToRemove := []string{"(", "*", ")"}
|
||||
name := fn.Name()
|
||||
for _, char := range charsToRemove {
|
||||
name = strings.ReplaceAll(name, char, "")
|
||||
}
|
||||
funcName := "[" + padString(strings.ReplaceAll(name, "/", "."), 40) + "] "
|
||||
return funcName
|
||||
}
|
||||
|
||||
func getLevelName(level uint8) string {
|
||||
switch level {
|
||||
case TraceLevel:
|
||||
return "TRACE"
|
||||
case DebugLevel:
|
||||
return "DEBUG"
|
||||
case InfoLevel:
|
||||
return "INFO"
|
||||
case ErrorLevel:
|
||||
return "ERROR"
|
||||
case WarnLevel:
|
||||
return "WARN"
|
||||
default:
|
||||
return "PANIC"
|
||||
}
|
||||
}
|
||||
|
||||
func sendLogWorker(level uint8, message []any) {
|
||||
if logger.Level >= level {
|
||||
var text1 strings.Builder
|
||||
for _, m := range message {
|
||||
var msgStr string
|
||||
var temp interface{}
|
||||
var trimmedStr string
|
||||
switch v := m.(type) {
|
||||
case string:
|
||||
err := json.Unmarshal([]byte(v), &temp)
|
||||
if err != nil {
|
||||
msgStr = fmt.Sprintf("%s", v)
|
||||
break
|
||||
}
|
||||
bs, _ := json.Marshal(&temp)
|
||||
trimmedStr = string(bs)
|
||||
case []byte:
|
||||
err := json.Unmarshal(v, &temp)
|
||||
if err != nil {
|
||||
msgStr = fmt.Sprintf("%s", v)
|
||||
break
|
||||
}
|
||||
bs, _ := json.Marshal(&temp)
|
||||
trimmedStr = string(bs)
|
||||
default:
|
||||
bs, _ := json.Marshal(v)
|
||||
err := json.Unmarshal(bs, &temp)
|
||||
if err != nil {
|
||||
msgStr = fmt.Sprintf("%v", v)
|
||||
break
|
||||
}
|
||||
trimmedStr = string(bs)
|
||||
}
|
||||
switch temp.(type) {
|
||||
case map[string]any, []any:
|
||||
var out bytes.Buffer
|
||||
err := json.Indent(&out, []byte(trimmedStr), "", "\t")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
msgStr = fmt.Sprintf("\n%s", out.String())
|
||||
default:
|
||||
msgStr = fmt.Sprintf("%v", m)
|
||||
}
|
||||
text1.WriteString(msgStr + " ")
|
||||
}
|
||||
text := "[" + getCurrentTime() + "] [" + padString(getLevelName(level), 7) + "] " + getMethodNameLogger() + text1.String()
|
||||
logWg.Add(1)
|
||||
logChan <- text
|
||||
}
|
||||
}
|
||||
|
||||
func DebugLogger(message ...any) {
|
||||
sendLogWorker(DebugLevel, message)
|
||||
}
|
||||
|
||||
func InfoLogger(message ...any) {
|
||||
sendLogWorker(InfoLevel, message)
|
||||
}
|
||||
|
||||
func TraceLogger(message ...any) {
|
||||
sendLogWorker(TraceLevel, message)
|
||||
}
|
||||
|
||||
func ErrorLogger(message ...any) {
|
||||
sendLogWorker(ErrorLevel, message)
|
||||
}
|
||||
|
||||
func WarnLogger(message ...any) {
|
||||
sendLogWorker(WarnLevel, message)
|
||||
}
|
||||
|
||||
func PanicLogger(message ...any) {
|
||||
sendLogWorker(PanicLevel, message)
|
||||
}
|
||||
|
||||
func padString(str string, totalLength int) string {
|
||||
return fmt.Sprintf("%-*s", totalLength, str)[0:totalLength]
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package fun
|
||||
|
||||
const (
|
||||
RequestNormalType uint8 = iota
|
||||
RequestStreamType
|
||||
)
|
||||
|
||||
type RequestInfo[T any] struct {
|
||||
MethodName string
|
||||
ServiceName string
|
||||
Data *T
|
||||
State map[string]string
|
||||
Type uint8
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
successCode uint8 = iota
|
||||
cellErrorCode
|
||||
errorCode
|
||||
)
|
||||
|
||||
type Result[T any] struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Code *uint16 `json:"code,omitempty"`
|
||||
Data *T `json:"data,omitempty"`
|
||||
Msg *string `json:"msg,omitempty"`
|
||||
Status uint8 `json:"status"`
|
||||
}
|
||||
|
||||
// Error 让 Result 实现 error 接口,业务方法可直接返回,Code/Msg/Status 随结果透传
|
||||
func (r Result[T]) Error() string {
|
||||
if r.Msg != nil {
|
||||
return *r.Msg
|
||||
}
|
||||
if r.Code != nil {
|
||||
return fmt.Sprintf("code=%d", *r.Code)
|
||||
}
|
||||
return "fun: unknown error"
|
||||
}
|
||||
|
||||
// Error 构造带错误码的错误响应,作为 error 返回
|
||||
// 用法:return "", fun.Error(4001, "登录失败")
|
||||
func Error(code uint16, msg string) error {
|
||||
return Result[any]{Code: &code, Msg: &msg, Status: errorCode}
|
||||
}
|
||||
|
||||
func callError(err error) Result[any] {
|
||||
return Result[any]{Msg: new(err.Error()), Status: cellErrorCode}
|
||||
}
|
||||
|
||||
// success 构造成功响应,空切片规范化为 [] 而不是 null
|
||||
func success(data any) Result[any] {
|
||||
return Result[any]{Data: nonNil(data), Status: successCode}
|
||||
}
|
||||
|
||||
// nonNil 返回 data 的指针;空切片会重建为同类型的非 nil 空切片,
|
||||
// 保证 JSON 序列化输出 [] 而不是 null
|
||||
func nonNil(data any) *any {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(data)
|
||||
if v.Kind() == reflect.Slice && v.Len() == 0 {
|
||||
return new(reflect.MakeSlice(v.Type(), 0, 0).Interface())
|
||||
}
|
||||
return &data
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Stream 流式响应的业务句柄。
|
||||
// 业务方法返回 *Stream 后,框架调用 Inject 注入推送通道;
|
||||
// 未注入时 Send/Close 自动阻塞等待,避免业务 goroutine 与注入之间的竞态。
|
||||
type Stream struct {
|
||||
mu sync.Mutex
|
||||
once sync.Once
|
||||
ready chan struct{}
|
||||
ch chan any
|
||||
done chan struct{}
|
||||
closed bool
|
||||
onClose func()
|
||||
}
|
||||
|
||||
func (s *Stream) getReady() chan struct{} {
|
||||
s.once.Do(func() {
|
||||
if s.ready == nil {
|
||||
s.ready = make(chan struct{})
|
||||
}
|
||||
})
|
||||
return s.ready
|
||||
}
|
||||
|
||||
// Inject 注入推送通道与结束信号,由框架在方法返回后调用
|
||||
func (s *Stream) Inject(ch chan any, done chan struct{}) {
|
||||
s.ch = ch
|
||||
s.done = done
|
||||
close(s.getReady())
|
||||
}
|
||||
|
||||
// Send 推送一条消息;连接断开或流已关闭时返回错误
|
||||
func (s *Stream) Send(message any) error {
|
||||
<-s.getReady()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return fmt.Errorf("fun: stream closed")
|
||||
}
|
||||
select {
|
||||
case s.ch <- message:
|
||||
return nil
|
||||
case <-s.done:
|
||||
return fmt.Errorf("fun: stream closed")
|
||||
}
|
||||
}
|
||||
|
||||
// Close 主动结束流,触发 OnClose 回调
|
||||
func (s *Stream) Close() {
|
||||
<-s.getReady()
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.closed = true
|
||||
cb := s.onClose
|
||||
s.mu.Unlock()
|
||||
if cb != nil {
|
||||
cb()
|
||||
}
|
||||
close(s.ch)
|
||||
}
|
||||
|
||||
// OnClose 注册关闭回调;流已关闭时立即执行
|
||||
func (s *Stream) OnClose(cb func()) {
|
||||
<-s.getReady()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
cb()
|
||||
return
|
||||
}
|
||||
s.onClose = cb
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
TagList map[string]string
|
||||
}
|
||||
|
||||
func newTag(tag reflect.StructTag) *Tag {
|
||||
t := &Tag{
|
||||
TagList: map[string]string{},
|
||||
}
|
||||
pairs := strings.Split(strings.TrimSpace(tag.Get("fun")), ";")
|
||||
for _, pair := range pairs {
|
||||
if pair == "" {
|
||||
continue
|
||||
}
|
||||
keyValue := strings.Split(pair, ":")
|
||||
if len(keyValue) == 1 {
|
||||
t.TagList[keyValue[0]] = ""
|
||||
} else {
|
||||
t.TagList[keyValue[0]] = keyValue[1]
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (tag *Tag) getTag(key string) (string, bool) {
|
||||
v, ok := tag.TagList[key]
|
||||
return v, ok
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
package fun
|
||||
|
||||
type templateGo struct{}
|
||||
|
||||
func (ctx templateGo) genDefaultServiceTemplate() string {
|
||||
return `package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Result 统一响应结构
|
||||
type Result[T any] struct {
|
||||
Id string
|
||||
Code *uint16
|
||||
Data *T
|
||||
Msg *string
|
||||
Status uint8
|
||||
}
|
||||
|
||||
func (r Result[T]) Error() string {
|
||||
if r.Msg != nil {
|
||||
return *r.Msg
|
||||
}
|
||||
if r.Code != nil {
|
||||
return fmt.Sprintf("code=%d", *r.Code)
|
||||
}
|
||||
return "api: unknown error"
|
||||
}
|
||||
|
||||
// Void 用于无数据返回的方法
|
||||
type Void = struct{}
|
||||
|
||||
// RequestInterceptor 请求前拦截器:可鉴权、加签、改 dto;返回 error 则直接失败
|
||||
type RequestInterceptor func(serviceName string, methodName string, dto any) error
|
||||
|
||||
// ResponseInterceptor 响应后拦截器:可记录日志、埋点、解密;返回 error 则转为失败响应
|
||||
type ResponseInterceptor func(serviceName string, methodName string, result Result[any]) error
|
||||
|
||||
// Client 内联 HTTP 客户端,不依赖外部 funclient 包
|
||||
type Client struct {
|
||||
url string
|
||||
client *http.Client
|
||||
state map[string]string
|
||||
requestInterceptors []RequestInterceptor
|
||||
responseInterceptors []ResponseInterceptor
|
||||
}
|
||||
|
||||
// NewClient 创建客户端
|
||||
func NewClient(url string) (*Client, error) {
|
||||
return &Client{
|
||||
url: strings.TrimRight(url, "/"),
|
||||
client: &http.Client{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetHttpClient 替换底层 http.Client
|
||||
func (c *Client) SetHttpClient(client *http.Client) {
|
||||
c.client = client
|
||||
}
|
||||
|
||||
// AddRequestInterceptor 注册请求前拦截器
|
||||
func (c *Client) AddRequestInterceptor(i RequestInterceptor) {
|
||||
c.requestInterceptors = append(c.requestInterceptors, i)
|
||||
}
|
||||
|
||||
// AddResponseInterceptor 注册响应后拦截器
|
||||
func (c *Client) AddResponseInterceptor(i ResponseInterceptor) {
|
||||
c.responseInterceptors = append(c.responseInterceptors, i)
|
||||
}
|
||||
|
||||
// SetState 设置随每个请求携带的状态(如 token),服务端 Guard 可读取
|
||||
func (c *Client) SetState(state map[string]string) {
|
||||
c.state = state
|
||||
}
|
||||
|
||||
// Request 发起普通调用
|
||||
func Request[T any](c *Client, serviceName string, methodName string, dto ...any) Result[T] {
|
||||
payload := newPayload(serviceName, methodName, dto, c.state)
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return Result[T]{Status: 2, Msg: ptr(err.Error())}
|
||||
}
|
||||
for _, i := range c.requestInterceptors {
|
||||
var dtoVal any
|
||||
if len(dto) > 0 {
|
||||
dtoVal = dto[0]
|
||||
}
|
||||
if err := i(serviceName, methodName, dtoVal); err != nil {
|
||||
return Result[T]{Status: 2, Msg: ptr(err.Error())}
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, c.url+"/cell", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return Result[T]{Status: 2, Msg: ptr(err.Error())}
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return Result[T]{Status: 2, Msg: ptr(err.Error())}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var out Result[T]
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return Result[T]{Status: 2, Msg: ptr(err.Error())}
|
||||
}
|
||||
var anyData *any
|
||||
if out.Data != nil {
|
||||
v := any(*out.Data)
|
||||
anyData = &v
|
||||
}
|
||||
anyResult := Result[any]{Id: out.Id, Code: out.Code, Data: anyData, Msg: out.Msg, Status: out.Status}
|
||||
for _, i := range c.responseInterceptors {
|
||||
if err := i(serviceName, methodName, anyResult); err != nil {
|
||||
return Result[T]{Status: 2, Msg: ptr(err.Error())}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Stream 发起流式调用,通过 NDJSON 行逐个推送消息(Streamable HTTP)
|
||||
func Stream[T any](c *Client, serviceName string, methodName string, dto ...any) (<-chan T, error) {
|
||||
payload := newPayload(serviceName, methodName, dto, c.state)
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, i := range c.requestInterceptors {
|
||||
var dtoVal any
|
||||
if len(dto) > 0 {
|
||||
dtoVal = dto[0]
|
||||
}
|
||||
if err := i(serviceName, methodName, dtoVal); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, c.url+"/cell", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("api: unexpected status %d", resp.StatusCode)
|
||||
}
|
||||
anyResult := Result[any]{Status: 0}
|
||||
for _, i := range c.responseInterceptors {
|
||||
if err := i(serviceName, methodName, anyResult); err != nil {
|
||||
resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ch := make(chan T)
|
||||
go func() {
|
||||
defer resp.Body.Close()
|
||||
defer close(ch)
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var msg T
|
||||
if err := json.Unmarshal([]byte(line), &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
ch <- msg
|
||||
}
|
||||
}()
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func newPayload(serviceName string, methodName string, dto []any, state map[string]string) map[string]any {
|
||||
payload := map[string]any{
|
||||
"serviceName": serviceName,
|
||||
"methodName": methodName,
|
||||
}
|
||||
if len(dto) > 0 {
|
||||
payload["data"] = dto[0]
|
||||
}
|
||||
if len(state) > 0 {
|
||||
payload["state"] = state
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func ptr(s string) *string { return &s }
|
||||
|
||||
type Api struct {
|
||||
{{- range .GenServiceList}}
|
||||
{{.ServiceName}} *{{.ServiceName}}
|
||||
{{- end}}
|
||||
*Client
|
||||
}
|
||||
|
||||
func CreateApi(url string) (Api, error) {
|
||||
apiClient, err := NewClient(url)
|
||||
return Api{
|
||||
{{- range .GenServiceList}}
|
||||
{{.ServiceName}}: New{{.ServiceName}}(apiClient),
|
||||
{{- end}}
|
||||
Client: apiClient,
|
||||
}, err
|
||||
}`
|
||||
}
|
||||
|
||||
func (ctx templateGo) genServiceTemplate() string {
|
||||
return `package api
|
||||
|
||||
type {{.ServiceName}} struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
func New{{.ServiceName}}(client *Client) *{{.ServiceName}} {
|
||||
return &{{.ServiceName}}{
|
||||
Client: client,
|
||||
}
|
||||
}
|
||||
|
||||
{{- $serviceName := .ServiceName }}
|
||||
{{- range .GenMethodTypeList}}
|
||||
{{if .IsStream }}func (ctx *{{$serviceName}}) {{.MethodName}}({{.DtoText}}) (<-chan {{.GenericTypeText}}, error) {
|
||||
return Stream[{{.GenericTypeText}}](ctx.Client, "{{$serviceName}}", "{{.MethodName}}"{{.ArgsText}})
|
||||
}{{else}}func (ctx *{{$serviceName}}) {{.MethodName}}({{.DtoText}}) {{.ReturnValueText}} {
|
||||
return Request[{{.GenericTypeText}}](ctx.Client, "{{$serviceName}}", "{{.MethodName}}"{{.ArgsText}})
|
||||
}{{end}}
|
||||
{{- end}}`
|
||||
}
|
||||
|
||||
func (ctx templateGo) genStructTemplate() string {
|
||||
return `package api
|
||||
|
||||
type {{.Name}} struct{
|
||||
{{- range .GenClassFieldType}}
|
||||
{{.Name}} {{.Type}} {{.Tag}}
|
||||
{{- end}}
|
||||
}`
|
||||
}
|
||||
|
||||
func (ctx templateGo) genEnumTemplate() string {
|
||||
return `package api
|
||||
|
||||
type {{.Name}} uint8
|
||||
|
||||
{{$enumName := .Name}}
|
||||
const (
|
||||
{{- range $index, $element := .Names}}
|
||||
{{$element}}{{if eq $index 0}} {{$enumName}} = iota{{end}}
|
||||
{{- end}}
|
||||
)
|
||||
|
||||
func ({{.Name}}) Values() []{{.Name}} {
|
||||
return []{{.Name}}{
|
||||
{{- range $index, $element := .Names}}
|
||||
{{$element}},
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
|
||||
{{if .DisplayNames}}
|
||||
func ({{.Name}}) DisplayNames() []string {
|
||||
return []string{
|
||||
{{- range $index, $element := .DisplayNames}}
|
||||
"{{$element}}",
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
{{end}}`
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package fun
|
||||
|
||||
type templateTs struct{}
|
||||
|
||||
func (ctx templateTs) genClientTemplate() string {
|
||||
return `export type result<T> = {
|
||||
id?: string;
|
||||
code?: number;
|
||||
data?: T;
|
||||
msg?: string;
|
||||
status: number;
|
||||
};
|
||||
|
||||
export type RequestInterceptor = (
|
||||
serviceName: string,
|
||||
methodName: string,
|
||||
dto: any
|
||||
) => Promise<void> | void;
|
||||
|
||||
export type ResponseInterceptor = (
|
||||
serviceName: string,
|
||||
methodName: string,
|
||||
result: result<any>
|
||||
) => Promise<void> | void;
|
||||
|
||||
export class Client {
|
||||
private url: string;
|
||||
private state: Record<string, string> = {};
|
||||
private requestInterceptors: RequestInterceptor[] = [];
|
||||
private responseInterceptors: ResponseInterceptor[] = [];
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
setState(state: Record<string, string>) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
addRequestInterceptor(i: RequestInterceptor) {
|
||||
this.requestInterceptors.push(i);
|
||||
}
|
||||
|
||||
addResponseInterceptor(i: ResponseInterceptor) {
|
||||
this.responseInterceptors.push(i);
|
||||
}
|
||||
|
||||
async request<T>(serviceName: string, methodName: string, dto?: any): Promise<result<T>> {
|
||||
for (const i of this.requestInterceptors) {
|
||||
await i(serviceName, methodName, dto);
|
||||
}
|
||||
const res = await fetch(this.url + "/cell", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ serviceName, methodName, data: dto, ...(Object.keys(this.state).length ? { state: this.state } : {}) }),
|
||||
});
|
||||
const out = (await res.json()) as result<T>;
|
||||
const anyResult: result<any> = {
|
||||
id: out.id,
|
||||
code: out.code,
|
||||
data: out.data,
|
||||
msg: out.msg,
|
||||
status: out.status,
|
||||
};
|
||||
for (const i of this.responseInterceptors) {
|
||||
await i(serviceName, methodName, anyResult);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async stream<T>(
|
||||
serviceName: string,
|
||||
methodName: string,
|
||||
dto: any | undefined,
|
||||
onMessage: (data: T) => void
|
||||
): Promise<void> {
|
||||
for (const i of this.requestInterceptors) {
|
||||
await i(serviceName, methodName, dto);
|
||||
}
|
||||
const res = await fetch(this.url + "/cell", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ serviceName, methodName, data: dto, ...(Object.keys(this.state).length ? { state: this.state } : {}) }),
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const anyResult: result<any> = { status: 0 };
|
||||
for (const i of this.responseInterceptors) {
|
||||
await i(serviceName, methodName, anyResult);
|
||||
}
|
||||
if (!res.body) return;
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
const payload = line.trim();
|
||||
if (!payload) continue;
|
||||
const data = JSON.parse(payload) as T;
|
||||
onMessage(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
func (ctx templateTs) genDefaultServiceTemplate() string {
|
||||
return `import { Client, type result } from "./client";
|
||||
{{- range .GenServiceList}}
|
||||
import {{.ServiceName}} from "./{{.ServiceName}}";
|
||||
{{- end}}
|
||||
|
||||
export class defaultApi extends Client {
|
||||
constructor(url: string) {
|
||||
super(url);
|
||||
}
|
||||
{{- range .GenServiceList}}
|
||||
public {{.ServiceName}}: {{.ServiceName}} = new {{.ServiceName}}(this);
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
export default class api {
|
||||
static create(url: string): defaultApi {
|
||||
return new defaultApi(url);
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
func (ctx templateTs) genServiceTemplate() string {
|
||||
return `import { Client, type result } from "./client"
|
||||
{{- range .GenImport}}
|
||||
import type {{.Name}} from "./{{.Name}}";
|
||||
{{- end}}
|
||||
|
||||
export default class {{.ServiceName}} {
|
||||
private client: Client;
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
{{- $serviceName := .ServiceName }}
|
||||
{{- range .GenMethodTypeList}}
|
||||
{{if .IsStream }}async {{.MethodName}}({{.DtoText}}{{if .DtoText}},{{end}}onMessage: (data: {{.GenericTypeText}}) => void): Promise<void> {
|
||||
return await this.client.stream<{{.GenericTypeText}}>("{{$serviceName}}", "{{.MethodName}}", {{if .DtoText}}dto{{else}}undefined{{end}}, onMessage)
|
||||
}{{else}}async {{.MethodName}}({{.DtoText}}): Promise<{{.ReturnValueText}}> {
|
||||
return await this.client.request<{{.GenericTypeText}}>("{{$serviceName}}", "{{.MethodName}}"{{.ArgsText}})
|
||||
}{{end}}
|
||||
{{- end}}
|
||||
}`
|
||||
}
|
||||
|
||||
func (ctx templateTs) genStructTemplate() string {
|
||||
return `{{- range .GenImport}}import type {{.Name}} from "./{{.Name}}";{{"\n"}}{{- end}}export default interface {{.Name}} {
|
||||
{{- range .GenClassFieldType}}
|
||||
{{.Name}}:{{.Type}}
|
||||
{{- end}}
|
||||
}`
|
||||
}
|
||||
|
||||
func (ctx templateTs) genEnumTemplate() string {
|
||||
return `enum {{.Name}} {
|
||||
{{- range $index, $element := .Names}}
|
||||
{{$element}},
|
||||
{{- end}}
|
||||
}{{ $enumName := .Name }}
|
||||
function values(): {{.Name}}[] {
|
||||
return [
|
||||
{{- range $index, $element := .Names}}
|
||||
{{$enumName}}.{{$element}},
|
||||
{{- end}}
|
||||
]
|
||||
}
|
||||
{{if .DisplayNames}}
|
||||
function displayNames(): string[] {
|
||||
return [
|
||||
{{- range $index, $element := .DisplayNames}}
|
||||
"{{$element}}",
|
||||
{{- end}}
|
||||
]
|
||||
}
|
||||
{{end}}
|
||||
export default {{.Name}}`
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 文本处理辅助方法
|
||||
|
||||
package fun
|
||||
|
||||
import "unicode"
|
||||
|
||||
// 首字母转大写
|
||||
func firstLetterToUpper(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
runes := []rune(s)
|
||||
runes[0] = unicode.ToUpper(runes[0])
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// 首字母转小写
|
||||
func firstLetterToLower(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
runes := []rune(s)
|
||||
runes[0] = unicode.ToLower(runes[0])
|
||||
return string(runes)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// getIP 获取客户端真实 IP
|
||||
// 优先级:X-Forwarded-For > X-Real-IP > RemoteAddr
|
||||
func getIP(r *http.Request) string {
|
||||
// 1. 优先获取真实 IP(多层代理时取最后一个非空段)
|
||||
if ip := lastNonEmpty(r.Header.Get("X-Forwarded-For")); ip != "" {
|
||||
return toLoopback(ip)
|
||||
}
|
||||
|
||||
// 2. X-Real-IP(通常由 Nginx 设置)
|
||||
if ip := strings.TrimSpace(r.Header.Get("X-Real-IP")); ip != "" {
|
||||
return toLoopback(ip)
|
||||
}
|
||||
|
||||
// 3. 最终回退到 RemoteAddr(兼容带端口、IPv6 方括号、无端口)
|
||||
if ip := hostOf(r.RemoteAddr); ip != "" {
|
||||
return toLoopback(ip)
|
||||
}
|
||||
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
// lastNonEmpty 取 X-Forwarded-For 中最后一个非空段
|
||||
// X-Forwarded-For: client, proxy1, proxy2
|
||||
func lastNonEmpty(xff string) string {
|
||||
parts := strings.Split(xff, ",")
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
if ip := strings.TrimSpace(parts[i]); ip != "" {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// hostOf 从 RemoteAddr 中提取 IP 部分
|
||||
// "203.0.113.9:4567" → "203.0.113.9","[::1]:4567" → "::1",
|
||||
// "198.51.100.88"(无端口)→ 原样返回
|
||||
func hostOf(remoteAddr string) string {
|
||||
raw := strings.TrimSpace(remoteAddr)
|
||||
host, _, err := net.SplitHostPort(raw)
|
||||
if err == nil && host != "" {
|
||||
return host
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// toLoopback 回环地址统一返回 127.0.0.1,其余原样返回
|
||||
func toLoopback(ip string) string {
|
||||
if parsed := net.ParseIP(ip); parsed != nil && parsed.IsLoopback() {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
return ip
|
||||
}
|
||||
Reference in New Issue
Block a user