package main import ( "bytes" "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" _ "bsm/full/module/base/ads/pb" _ "bsm/full/module/base/cloud/pb" _ "bsm/full/module/base/cms/pb" _ "bsm/full/module/base/feedback/pb" _ "bsm/full/module/base/initial/pb" _ "bsm/full/module/base/passport/pb" _ "bsm/full/module/base/sender/pb" _ "bsm/full/module/ec/address/pb" _ "bsm/full/module/ec/mall/pb" _ "bsm/full/module/ec/market/pb" _ "bsm/full/module/ec/order/pb" _ "bsm/full/module/finance/wallet/pb" _ "bsm/full/module/social/feed/pb" _ "bsm/full/module/social/group/pb" _ "bsm/full/module/social/relation/pb" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" ) type moduleInfo struct { Order int Name string Package protoreflect.FullName Description string } var modules = []moduleInfo{ {1, "ads", "ads", "广告获取与投放内容读取。"}, {2, "cloud", "cloud", "云端相册、书签、网盘、笔记、私密数据、分享和空间管理。"}, {3, "cms", "cms", "站点、页面、文章、栏目和标签内容管理。"}, {4, "feedback", "feedback", "用户反馈提交与查询。"}, {5, "initial", "initial", "客户端初始化、基础数据和版本更新检查。"}, {6, "passport", "passport", "账号注册、登录、验证、找回密码和账户资料管理。"}, {7, "sender", "sender", "邮件、短信发送及验证码校验。"}, {8, "address", "address", "用户地址库管理。"}, {9, "mall", "mall", "商城广告、类目、运费、公告、商品、员工和店铺管理。"}, {10, "market", "market", "市场代理商、供应商及经营数据管理。"}, {11, "order", "order", "购物车、优惠券、订单汇总及商家订单管理。"}, {12, "wallet", "wallet", "钱包、支付方式、支付宝与微信支付。"}, {13, "feed", "feed", "动态发布、标签、时间线和动态设置。"}, {14, "group", "group", "群组及成员管理。"}, {15, "relation", "relation", "关注、好友和关系匹配。"}, } func main() { root, err := filepath.Abs(filepath.Join("..", "..")) must(err) outDir := filepath.Join(root, "wiki", "api") must(os.MkdirAll(outDir, 0o755)) for _, module := range modules { services := servicesFor(module.Package) content := renderModule(module, services) name := fmt.Sprintf("%02d-%s.md", module.Order, module.Name) must(os.WriteFile(filepath.Join(outDir, name), content, 0o644)) } } func servicesFor(pkg protoreflect.FullName) []protoreflect.ServiceDescriptor { var services []protoreflect.ServiceDescriptor protoregistry.GlobalFiles.RangeFiles(func(file protoreflect.FileDescriptor) bool { if file.Package() != pkg { return true } for i := 0; i < file.Services().Len(); i++ { services = append(services, file.Services().Get(i)) } return true }) sort.Slice(services, func(i, j int) bool { return services[i].FullName() < services[j].FullName() }) return services } func renderModule(module moduleInfo, services []protoreflect.ServiceDescriptor) []byte { var out bytes.Buffer fmt.Fprintf(&out, "# %s API\n\n%s\n\n", strings.ToUpper(module.Name), module.Description) out.WriteString("> 本文档由 protobuf descriptor 生成。字段名采用 protobuf JSON 名称;64 位整数在 JSON 中应按字符串处理。\n\n") out.WriteString("## 接入方式\n\n") fmt.Fprintf(&out, "- 动态 HTTP:`POST /rpc/%s/{Service}/{Method}`\n", module.Package) fmt.Fprintf(&out, "- 原生 gRPC:`/%s.{Service}/{Method}`\n", module.Package) fmt.Fprintf(&out, "- grpc-gateway:`POST /%s.{Service}/{Method}`\n", module.Package) out.WriteString("- 鉴权:需要登录的接口通过 `Authorization: Bearer ` 传递凭证。\n") out.WriteString("- 动态 HTTP 成功或失败均返回 HTTP 200,业务状态见 `code`、`message`、`details`。\n\n") out.WriteString("## 服务概览\n\n") out.WriteString("| 服务 | 方法数 | 说明 |\n|---|---:|---|\n") for _, service := range services { fmt.Fprintf(&out, "| `%s` | %d | %s 服务 |\n", service.Name(), service.Methods().Len(), service.Name()) } schemas := map[protoreflect.FullName]protoreflect.MessageDescriptor{} enums := map[protoreflect.FullName]protoreflect.EnumDescriptor{} for _, service := range services { fmt.Fprintf(&out, "\n## %s\n\n", service.Name()) out.WriteString("| 方法 | 动态 HTTP 路径 | 请求 | 响应 | 类型 |\n|---|---|---|---|---|\n") for i := 0; i < service.Methods().Len(); i++ { method := service.Methods().Get(i) kind := "Unary" if method.IsStreamingClient() || method.IsStreamingServer() { kind = "Streaming(动态 HTTP 暂不支持)" } fmt.Fprintf(&out, "| `%s` | `POST /rpc/%s/%s/%s` | [`%s`](#%s) | [`%s`](#%s) | %s |\n", method.Name(), module.Package, service.Name(), method.Name(), method.Input().FullName(), anchor(method.Input().FullName()), method.Output().FullName(), anchor(method.Output().FullName()), kind) collectMessage(method.Input(), schemas, enums) collectMessage(method.Output(), schemas, enums) } for i := 0; i < service.Methods().Len(); i++ { method := service.Methods().Get(i) fmt.Fprintf(&out, "\n### %s.%s\n\n", service.Name(), method.Name()) fmt.Fprintf(&out, "- 动态 HTTP:`POST /rpc/%s/%s/%s`\n", module.Package, service.Name(), method.Name()) fmt.Fprintf(&out, "- gRPC:`/%s/%s`\n", service.FullName(), method.Name()) fmt.Fprintf(&out, "- 白名单键:`%s.%s`\n", service.FullName(), method.Name()) out.WriteString("- 请求示例:\n\n```json\n") out.Write(exampleJSON(method.Input())) out.WriteString("\n```\n") } } messageNames := sortedMessageNames(schemas) if len(messageNames) > 0 { out.WriteString("\n## 消息结构\n") } for _, name := range messageNames { renderMessage(&out, schemas[name], enums) } enumNames := make([]string, 0, len(enums)) for name := range enums { enumNames = append(enumNames, string(name)) } sort.Strings(enumNames) if len(enumNames) > 0 { out.WriteString("\n## 枚举\n") } for _, rawName := range enumNames { renderEnum(&out, enums[protoreflect.FullName(rawName)]) } return out.Bytes() } func collectMessage(message protoreflect.MessageDescriptor, messages map[protoreflect.FullName]protoreflect.MessageDescriptor, enums map[protoreflect.FullName]protoreflect.EnumDescriptor) { if _, exists := messages[message.FullName()]; exists { return } messages[message.FullName()] = message for i := 0; i < message.Fields().Len(); i++ { field := message.Fields().Get(i) if field.Message() != nil { collectMessage(field.Message(), messages, enums) } if field.Enum() != nil { enums[field.Enum().FullName()] = field.Enum() } } } func renderMessage(out *bytes.Buffer, message protoreflect.MessageDescriptor, enums map[protoreflect.FullName]protoreflect.EnumDescriptor) { fmt.Fprintf(out, "\n### %s\n\n", message.FullName()) if message.Fields().Len() == 0 { out.WriteString("空消息:请求时发送 `{}`。\n") return } out.WriteString("| JSON 字段 | 类型 | 规则 |\n|---|---|---|\n") for i := 0; i < message.Fields().Len(); i++ { field := message.Fields().Get(i) rule := "可选" if field.Cardinality() == protoreflect.Repeated { rule = "数组" } if field.IsMap() { rule = "对象映射" } if field.ContainingOneof() != nil && !field.ContainingOneof().IsSynthetic() { rule = "oneof: " + string(field.ContainingOneof().Name()) } fmt.Fprintf(out, "| `%s` | `%s` | %s |\n", field.JSONName(), fieldType(field), rule) if field.Enum() != nil { enums[field.Enum().FullName()] = field.Enum() } } } func renderEnum(out *bytes.Buffer, enum protoreflect.EnumDescriptor) { fmt.Fprintf(out, "\n### %s\n\n", enum.FullName()) out.WriteString("| 名称 | 数值 |\n|---|---:|\n") for i := 0; i < enum.Values().Len(); i++ { value := enum.Values().Get(i) fmt.Fprintf(out, "| `%s` | %d |\n", value.Name(), value.Number()) } } func fieldType(field protoreflect.FieldDescriptor) string { var value string switch field.Kind() { case protoreflect.MessageKind, protoreflect.GroupKind: value = string(field.Message().FullName()) case protoreflect.EnumKind: value = string(field.Enum().FullName()) default: value = field.Kind().String() } if field.IsMap() { value = "map<" + fieldType(field.MapKey()) + ", " + fieldType(field.MapValue()) + ">" } else if field.Cardinality() == protoreflect.Repeated { value = "[]" + value } return value } func exampleJSON(message protoreflect.MessageDescriptor) []byte { value := exampleMessage(message, map[protoreflect.FullName]bool{}, 0) data, err := json.MarshalIndent(value, "", " ") must(err) return data } func exampleMessage(message protoreflect.MessageDescriptor, visiting map[protoreflect.FullName]bool, depth int) map[string]any { if depth > 2 || visiting[message.FullName()] { return map[string]any{} } visiting[message.FullName()] = true defer delete(visiting, message.FullName()) result := map[string]any{} seenOneof := map[protoreflect.Name]bool{} for i := 0; i < message.Fields().Len(); i++ { field := message.Fields().Get(i) if oneof := field.ContainingOneof(); oneof != nil && !oneof.IsSynthetic() { if seenOneof[oneof.Name()] { continue } seenOneof[oneof.Name()] = true } value := exampleField(field, visiting, depth) if field.Cardinality() == protoreflect.Repeated && !field.IsMap() { value = []any{value} } if field.IsMap() { value = map[string]any{"key": exampleField(field.MapValue(), visiting, depth)} } result[field.JSONName()] = value } return result } func exampleField(field protoreflect.FieldDescriptor, visiting map[protoreflect.FullName]bool, depth int) any { switch field.Kind() { case protoreflect.BoolKind: return false case protoreflect.StringKind: return "string" case protoreflect.BytesKind: return "BASE64" case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind, protoreflect.Uint64Kind, protoreflect.Fixed64Kind: return "0" case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind, protoreflect.Uint32Kind, protoreflect.Fixed32Kind, protoreflect.FloatKind, protoreflect.DoubleKind: return 0 case protoreflect.EnumKind: if field.Enum().Values().Len() > 0 { return string(field.Enum().Values().Get(0).Name()) } return 0 case protoreflect.MessageKind, protoreflect.GroupKind: return exampleMessage(field.Message(), visiting, depth+1) default: return nil } } func sortedMessageNames(messages map[protoreflect.FullName]protoreflect.MessageDescriptor) []protoreflect.FullName { names := make([]protoreflect.FullName, 0, len(messages)) for name := range messages { names = append(names, name) } sort.Slice(names, func(i, j int) bool { return names[i] < names[j] }) return names } func anchor(name protoreflect.FullName) string { return strings.ToLower(strings.ReplaceAll(string(name), ".", "")) } func must(err error) { if err != nil { panic(err) } }