docs: add comprehensive client API reference

This commit is contained in:
2026-08-11 19:09:29 +08:00
parent 5b5f6f8f00
commit 2d1deb54f0
27 changed files with 10727 additions and 4 deletions

View File

@@ -0,0 +1,10 @@
$ErrorActionPreference = "Stop"
$env:GOWORK = "off"
$env:GOLANG_PROTOBUF_REGISTRATION_CONFLICT = "warn"
try {
go run .
if ($LASTEXITCODE -ne 0) { throw "API documentation generation failed" }
} finally {
Remove-Item Env:GOWORK -ErrorAction SilentlyContinue
Remove-Item Env:GOLANG_PROTOBUF_REGISTRATION_CONFLICT -ErrorAction SilentlyContinue
}

7
scripts/api-docgen/generate.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -Eeuo pipefail
export GOWORK=off
export GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn
go run .

50
scripts/api-docgen/go.mod Normal file
View File

@@ -0,0 +1,50 @@
module bsm/full/tools/api-docgen
go 1.26.5
require (
bsm/full/module/base/ads v0.0.0
bsm/full/module/base/cloud v0.0.0
bsm/full/module/base/cms v0.0.0
bsm/full/module/base/feedback v0.0.0
bsm/full/module/base/initial v0.0.0
bsm/full/module/base/passport v0.0.0
bsm/full/module/base/sender v0.0.0
bsm/full/module/ec/address v0.0.0
bsm/full/module/ec/mall v0.0.0
bsm/full/module/ec/market v0.0.0
bsm/full/module/ec/order v0.0.0
bsm/full/module/finance/wallet v0.0.0
bsm/full/module/social/feed v0.0.0
bsm/full/module/social/group v0.0.0
bsm/full/module/social/relation v0.0.0
google.golang.org/protobuf v1.36.11
)
require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260807164820-c8921c73eeea // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea // indirect
google.golang.org/grpc v1.83.0 // indirect
)
replace (
bsm/full/module/base/ads => ../../module/base/ads
bsm/full/module/base/cloud => ../../module/base/cloud
bsm/full/module/base/cms => ../../module/base/cms
bsm/full/module/base/feedback => ../../module/base/feedback
bsm/full/module/base/initial => ../../module/base/initial
bsm/full/module/base/passport => ../../module/base/passport
bsm/full/module/base/sender => ../../module/base/sender
bsm/full/module/ec/address => ../../module/ec/address
bsm/full/module/ec/mall => ../../module/ec/mall
bsm/full/module/ec/market => ../../module/ec/market
bsm/full/module/ec/order => ../../module/ec/order
bsm/full/module/finance/wallet => ../../module/finance/wallet
bsm/full/module/social/feed => ../../module/social/feed
bsm/full/module/social/group => ../../module/social/group
bsm/full/module/social/relation => ../../module/social/relation
)

42
scripts/api-docgen/go.sum Normal file
View File

@@ -0,0 +1,42 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260807164820-c8921c73eeea h1:Jifw/kjs/r3B0uszvls/m3c3tmZs2YHGM9C+rvxP9gY=
google.golang.org/genproto/googleapis/api v0.0.0-20260807164820-c8921c73eeea/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea h1:kVhQEPTpKQahD5+JSBTfBB19wcgQTTjAIn45MBqnyHk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=

301
scripts/api-docgen/main.go Normal file
View File

@@ -0,0 +1,301 @@
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 <token>` 传递凭证。\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)
}
}