2026-02-17 13:06:23 +08:00
|
|
|
|
package swagger
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path"
|
|
|
|
|
|
"reflect"
|
|
|
|
|
|
"regexp"
|
|
|
|
|
|
"sort"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"xiawan/wx/srv/srvconfig"
|
|
|
|
|
|
|
|
|
|
|
|
"gopkg.in/yaml.v2"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type SwgMap map[string]interface{}
|
|
|
|
|
|
|
|
|
|
|
|
// 优先显示的API路径列表
|
|
|
|
|
|
var priorityPaths = []string{
|
|
|
|
|
|
"/message/SetCallback",
|
|
|
|
|
|
"/message/GetCallback",
|
|
|
|
|
|
"/message/DeleteCallback",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 是否为优先显示的API路径
|
|
|
|
|
|
func isPriorityPath(path string) bool {
|
|
|
|
|
|
for _, pp := range priorityPaths {
|
|
|
|
|
|
if strings.HasSuffix(path, pp) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取排序的路径映射
|
|
|
|
|
|
func getSortedPaths(paths SwgMap) SwgMap {
|
|
|
|
|
|
// 创建一个新的排序后的paths映射
|
|
|
|
|
|
sortedPaths := make(SwgMap)
|
|
|
|
|
|
|
|
|
|
|
|
// 首先添加消息回调路径
|
|
|
|
|
|
for path, pathItem := range paths {
|
|
|
|
|
|
if isCallbackPath(strings.TrimPrefix(path, srvconfig.GlobalSetting.ApiVersion)) {
|
|
|
|
|
|
sortedPaths[path] = pathItem
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 然后添加其他优先路径
|
|
|
|
|
|
for path, pathItem := range paths {
|
|
|
|
|
|
if !isCallbackPath(strings.TrimPrefix(path, srvconfig.GlobalSetting.ApiVersion)) && isPriorityPath(path) {
|
|
|
|
|
|
sortedPaths[path] = pathItem
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 最后添加其余路径
|
|
|
|
|
|
for path, pathItem := range paths {
|
|
|
|
|
|
if !isCallbackPath(strings.TrimPrefix(path, srvconfig.GlobalSetting.ApiVersion)) && !isPriorityPath(path) {
|
|
|
|
|
|
sortedPaths[path] = pathItem
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return sortedPaths
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存swagger生成后的回调,可通过此变量获取自定义生成的swagger
|
|
|
|
|
|
var GeneratedSwaggerData map[string]interface{}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查path是否为消息回调路径
|
|
|
|
|
|
func isCallbackPath(path string) bool {
|
|
|
|
|
|
callbackPaths := []string{
|
|
|
|
|
|
"/message/SetCallback",
|
|
|
|
|
|
"/message/GetCallback",
|
|
|
|
|
|
"/message/DeleteCallback",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 去除版本前缀
|
|
|
|
|
|
if strings.HasPrefix(path, srvconfig.GlobalSetting.ApiVersion) {
|
|
|
|
|
|
path = strings.TrimPrefix(path, srvconfig.GlobalSetting.ApiVersion)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, cbPath := range callbackPaths {
|
|
|
|
|
|
if path == cbPath {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取排序后的路径,优先显示消息回调路径
|
|
|
|
|
|
func getCallbackPrioritizedPaths(paths map[string]interface{}) []string {
|
|
|
|
|
|
// 先分离回调路径和普通路径
|
|
|
|
|
|
var callbackPaths []string
|
|
|
|
|
|
var normalPaths []string
|
|
|
|
|
|
|
|
|
|
|
|
for path := range paths {
|
|
|
|
|
|
if isCallbackPath(path) {
|
|
|
|
|
|
callbackPaths = append(callbackPaths, path)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
normalPaths = append(normalPaths, path)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 对回调路径和普通路径分别排序
|
|
|
|
|
|
sort.Strings(callbackPaths)
|
|
|
|
|
|
sort.Strings(normalPaths)
|
|
|
|
|
|
|
|
|
|
|
|
// 合并路径,回调路径在前
|
|
|
|
|
|
return append(callbackPaths, normalPaths...)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func generateSwagger() {
|
|
|
|
|
|
bytes, err := os.ReadFile("./api/router/router.go")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
checkApiVersion()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
lines := strings.Split(string(bytes), "\n")
|
|
|
|
|
|
var current string
|
|
|
|
|
|
routers := make(map[string][][]string)
|
|
|
|
|
|
for i := 0; i < len(lines); i++ {
|
|
|
|
|
|
line := strings.TrimSpace(lines[i])
|
|
|
|
|
|
if strings.Contains(line, ".Group(") {
|
|
|
|
|
|
reg := regexp.MustCompile("ver \\+ \"/(\\w+)")
|
|
|
|
|
|
if match := reg.FindStringSubmatch(line); len(match) >= 2 {
|
|
|
|
|
|
current = "/" + match[1]
|
|
|
|
|
|
next, handles := analyzeGroupRouter(lines, i+1)
|
|
|
|
|
|
i = next
|
|
|
|
|
|
routers[current] = append(routers[current], handles...)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tags := []SwgMap{}
|
|
|
|
|
|
|
|
|
|
|
|
// 首先添加消息回调标签,确保它始终在最前面
|
|
|
|
|
|
callbackTag := SwgMap{"name": "消息回调", "description": "消息回调接口"}
|
|
|
|
|
|
tags = append(tags, callbackTag)
|
|
|
|
|
|
|
|
|
|
|
|
for k := range routers {
|
|
|
|
|
|
// k 为 URL 前缀
|
|
|
|
|
|
// 跳过已添加的消息回调标签
|
|
|
|
|
|
tagName := getTag(k)
|
|
|
|
|
|
if tagName == "消息回调" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
tags = append(tags, SwgMap{"name": tagName, "description": k})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确保手动注册API的标签也被包含
|
|
|
|
|
|
for path := range GetManualAPIs() {
|
|
|
|
|
|
parts := strings.Split(path, "/")
|
|
|
|
|
|
if len(parts) > 1 {
|
|
|
|
|
|
tagPath := "/" + parts[1]
|
|
|
|
|
|
tagExists := false
|
|
|
|
|
|
tagName := getTag(tagPath)
|
|
|
|
|
|
// 跳过已添加的消息回调标签
|
|
|
|
|
|
if tagName == "消息回调" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, tag := range tags {
|
|
|
|
|
|
if tag["description"].(string) == tagPath {
|
|
|
|
|
|
tagExists = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !tagExists {
|
|
|
|
|
|
tags = append(tags, SwgMap{"name": tagName, "description": tagPath})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 对 tags 进行排序(消息回调标签已经在最前面,所以从索引1开始排序)
|
|
|
|
|
|
if len(tags) > 1 {
|
|
|
|
|
|
sort.Slice(tags[1:], func(i, j int) bool {
|
|
|
|
|
|
val1 := getValue(tags[i+1]["description"].(string))
|
|
|
|
|
|
val2 := getValue(tags[j+1]["description"].(string))
|
|
|
|
|
|
return val1 < val2
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 合并自动分析和手动注册的路径
|
|
|
|
|
|
paths := analyseControllers(routers)
|
|
|
|
|
|
for path, pathItem := range GetManualAPIs() {
|
|
|
|
|
|
apiPath := fmt.Sprintf("%s%s", srvconfig.GlobalSetting.ApiVersion, path)
|
|
|
|
|
|
paths[apiPath] = pathItem
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 对paths进行排序,确保优先路径在前
|
|
|
|
|
|
paths = getSortedPaths(paths)
|
|
|
|
|
|
|
|
|
|
|
|
swgMap := SwgMap{
|
|
|
|
|
|
"swagger": "2.0",
|
|
|
|
|
|
"info": SwgMap{
|
2026-02-26 10:44:13 +08:00
|
|
|
|
"title": "8069-07",
|
|
|
|
|
|
"description": "长连接-自动心跳-自动二登-消息回调-长时间在线 - Updated By 408449830 \n 26.2.2 拉到8069, 修复密钥长度问题 \n 26.1.20 增加修改我在群聊中的昵称功能, 修复红包测试遗留问题, 修复允许服务器ip直连配置问题 \n 26.1.6 添加代理连接配置和超时控制 \n 25.12.24 增加cdn高清图片上传接口, 修复初始化消息同步问题 \n 25.12.18 修复a16和62数据登录, 增加a16转62, 62转a16功能 \n25.12.13 修复二登后长链接失效问题 \n25.12.9 增加视频号助手扫码登录功能 \n 25.12.5 增加手机点击退出ipad登录后, 同步在线状态 \n 25.12.4 修复重启后代理丢失问题 \n 25.11.8 新增企微图片下载, 整合接收xml格式 \n 25.11.5 修复ws消息阻塞问题(断连后批量推送), 删除历史消息接收逻辑 \n 25.11.1 修复长语音下载问题 \n 25.9.1 增加mac滑块网页, 依旧用mac2ipad接口(但实测依旧不能pc同时在线) \n 25.8.21 增加mac滑块, mac2iPad(direct接口) \n 25.8.9 直登被和谐了, 另谋出路~ \n 25.8.6 消息回调格式封装外层key, 方便聚合聊天callback区分账号 \n 25.8.5 抽离直登和iPad登录,同步callback和ws消息格式 \n 25.8.3 直登iPad, 修复心跳相关问题, 重新拉到861, 下个版本跟新算法吧, 头痛 \n 25.8.1 增加群聊成员变动回调 \n 25.7.6 增加Windows登录(新号不建议用, 容易风控) 部分接口恢复859, 准备下个版本更新ccd+rqtx全方面升级860(本版本是需要收费的, 不更新不影响正常使用) \n 25.6.30 修复多次取码的状态问题 \n 25.6.28 拉到8060, 不能直接解决人脸, 很头痛 \n 25.6.16 重构数据库层, 增加使用SQLite分支, 方便打包分发 \n 25.6.14 增加车载登录, 自动切号绕过验证码/人脸 \n 25.5.28 修复输入验证码接口, 准备修复群聊消息同步问题 \n 25.5.24 增加输入登录验证码接口 \n 25.5.20 增加安卓平板, 配置文件增加是否切设备选项 \n 25.5.19 增加获取公众号文章阅读数量接口, 解决新设备确认和扫脸确认 \n 25.5.16 增加阅读/点赞接口,增加mac登录,增加删除手机通讯录接口 \n 25.5.15 增加文件上传功能, 优化文件转发功能, 准备增加输入登录验证码 \n 25.5.14 拉到8059, 准备更新ccd \n 25.5.13 每10次短连接前检查一次长连接状态(优先长链接) \n 25.5.12 增加回调持久化功能,修复登录状态缓存问题(逻辑错误) \n 25.5.11 增加消息回调功能,删除签名 \n 25.5.10 优化红包速度, 修复长连接消息同步失效 \n 25.5.9 修复因代波动导致长连接断开问题 \n 25.4.8 修复ws断链问题,解决跨域 \n 25.5.7 修复撤回图片逻辑 \n 25.5.6 增加朋友圈视频上传 \n 25.5.3 修复ws并发panic",
|
2026-02-17 13:06:23 +08:00
|
|
|
|
"contact": "",
|
|
|
|
|
|
"version": "仅供学习交流使用,禁止用于非法用途",
|
|
|
|
|
|
},
|
|
|
|
|
|
"basePath": srvconfig.GlobalSetting.ApiVersion,
|
|
|
|
|
|
"paths": paths,
|
|
|
|
|
|
"definitions": analyzeRequestModels(),
|
|
|
|
|
|
"tags": tags,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 序列化为 YAML 格式
|
|
|
|
|
|
data, _ := yaml.Marshal(&swgMap)
|
|
|
|
|
|
_ = os.Chmod("./static/swagger/swagger.yml", 0777)
|
|
|
|
|
|
err = os.WriteFile("./static/swagger/swagger.yml", data, 0x0777)
|
|
|
|
|
|
|
|
|
|
|
|
bytes, _ = json.Marshal(swgMap)
|
|
|
|
|
|
//bytes, _ = json.MarshalIndent(swgMap, "", " ") // 缩进
|
|
|
|
|
|
_ = os.Chmod("./static/swagger/swagger.json", 0777)
|
|
|
|
|
|
err = os.WriteFile("./static/swagger/swagger.json", bytes, 0x0777)
|
|
|
|
|
|
fmt.Println("generateSwagger success")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkApiVersion() {
|
|
|
|
|
|
_ = os.Chmod("./static/swagger/swagger.yml", 0777)
|
|
|
|
|
|
_ = os.Chmod("./static/swagger/swagger.json", 0777)
|
|
|
|
|
|
|
|
|
|
|
|
// 加载 swagger.json
|
|
|
|
|
|
jsonBytes, err := os.ReadFile("./static/swagger/swagger.json")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
swgMap := SwgMap{}
|
|
|
|
|
|
err = json.Unmarshal(jsonBytes, &swgMap)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
swgMap["basePath"] = srvconfig.GlobalSetting.ApiVersion
|
|
|
|
|
|
|
|
|
|
|
|
// 序列化为 YAML 格式
|
|
|
|
|
|
data, _ := yaml.Marshal(&swgMap)
|
|
|
|
|
|
err = os.WriteFile("./static/swagger/swagger.yml", data, 0x0777)
|
|
|
|
|
|
|
|
|
|
|
|
jsonBytes, _ = json.Marshal(swgMap)
|
|
|
|
|
|
//bytes, _ = json.MarshalIndent(swgMap, "", " ") // 缩进
|
|
|
|
|
|
err = os.WriteFile("./static/swagger/swagger.json", jsonBytes, 0x0777)
|
|
|
|
|
|
fmt.Println("updateApiVersion success")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func analyzeRequestModels() SwgMap {
|
|
|
|
|
|
byts, _ := os.ReadFile("./api/req/model.go")
|
|
|
|
|
|
byts2, _ := os.ReadFile("./api/req/cmd-model.go")
|
|
|
|
|
|
lines := strings.Split(string(byts), "\n")
|
|
|
|
|
|
lines = append(lines, strings.Split(string(byts2), "\n")...)
|
|
|
|
|
|
|
|
|
|
|
|
// 添加回调模型文件
|
|
|
|
|
|
callbackModelBytes, err := os.ReadFile("./api/req/callbackModel.go")
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
lines = append(lines, strings.Split(string(callbackModelBytes), "\n")...)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加文件上传模型
|
|
|
|
|
|
fileModelBytes, err := os.ReadFile("./api/req/fileModel.go")
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
lines = append(lines, strings.Split(string(fileModelBytes), "\n")...)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lines = append(lines, []string{
|
|
|
|
|
|
"type baseinfo.UserLabelInfoItem struct {",
|
|
|
|
|
|
" UserName string",
|
|
|
|
|
|
" LabelIDList string",
|
|
|
|
|
|
"}",
|
|
|
|
|
|
|
|
|
|
|
|
"type baseinfo.Location struct {",
|
|
|
|
|
|
" PoiClassifyID string",
|
|
|
|
|
|
" PoiName string",
|
|
|
|
|
|
" PoiAddress string",
|
|
|
|
|
|
" PoiClassifyType uint32",
|
|
|
|
|
|
" City string",
|
|
|
|
|
|
" Latitude string",
|
|
|
|
|
|
" Longitude string",
|
|
|
|
|
|
"}",
|
|
|
|
|
|
}...)
|
|
|
|
|
|
definitions := make(SwgMap)
|
|
|
|
|
|
for i := 0; i < len(lines); i++ {
|
|
|
|
|
|
line := lines[i]
|
|
|
|
|
|
if strings.HasPrefix(line, "type") {
|
|
|
|
|
|
matches := regexp.MustCompile("type ([\\w.]+) struct").FindStringSubmatch(line)
|
|
|
|
|
|
if len(matches) < 2 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
name := matches[1]
|
|
|
|
|
|
definitions[name] = SwgMap{
|
|
|
|
|
|
"title": name,
|
|
|
|
|
|
"type": "object",
|
|
|
|
|
|
"properties": analyzeModeStruct(lines, i+1, name == "MessageItem"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
analyzeBaseInfoStruct(definitions)
|
|
|
|
|
|
|
|
|
|
|
|
// 整合手动注册的模型
|
|
|
|
|
|
for name, model := range GetManualModels() {
|
|
|
|
|
|
if _, exists := definitions[name]; !exists {
|
|
|
|
|
|
modelType := reflect.TypeOf(model)
|
|
|
|
|
|
if modelType.Kind() == reflect.Struct {
|
|
|
|
|
|
properties := make(SwgMap)
|
|
|
|
|
|
for i := 0; i < modelType.NumField(); i++ {
|
|
|
|
|
|
field := modelType.Field(i)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取json标签
|
|
|
|
|
|
jsonTag := field.Tag.Get("json")
|
|
|
|
|
|
if jsonTag == "" || jsonTag == "-" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 去掉json标签中的选项部分
|
|
|
|
|
|
jsonName := strings.Split(jsonTag, ",")[0]
|
|
|
|
|
|
|
|
|
|
|
|
// 获取字段类型
|
|
|
|
|
|
fieldType := field.Type.Name()
|
|
|
|
|
|
if fieldType == "" {
|
|
|
|
|
|
switch field.Type.Kind() {
|
|
|
|
|
|
case reflect.String:
|
|
|
|
|
|
fieldType = "string"
|
|
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
|
|
|
|
fieldType = "integer"
|
|
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
|
|
fieldType = "integer"
|
|
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
|
|
|
|
fieldType = "number"
|
|
|
|
|
|
case reflect.Bool:
|
|
|
|
|
|
fieldType = "boolean"
|
|
|
|
|
|
case reflect.Map:
|
|
|
|
|
|
fieldType = "object"
|
|
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
|
|
|
|
fieldType = "array"
|
|
|
|
|
|
default:
|
|
|
|
|
|
fieldType = "object"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取注释
|
|
|
|
|
|
description := field.Tag.Get("description")
|
|
|
|
|
|
if description == "" {
|
|
|
|
|
|
// 尝试从字段名后的注释中提取
|
|
|
|
|
|
description = strings.TrimSpace(field.Tag.Get("comment"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建属性定义
|
|
|
|
|
|
typeMap := convertType(fieldType)
|
|
|
|
|
|
typeMap["description"] = description
|
|
|
|
|
|
|
|
|
|
|
|
properties[jsonName] = typeMap
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
definitions[name] = SwgMap{
|
|
|
|
|
|
"title": name,
|
|
|
|
|
|
"type": "object",
|
|
|
|
|
|
"properties": properties,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return definitions
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func analyzeModeStruct(lines []string, i int, debug bool) SwgMap {
|
|
|
|
|
|
props := make(SwgMap)
|
|
|
|
|
|
for _, line := range lines[i:] {
|
|
|
|
|
|
if strings.HasPrefix(strings.TrimSpace(line), "//") {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.HasPrefix(line, "}") {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
columns := listTrimSpace(strings.Split(strings.TrimSpace(line), " "))
|
|
|
|
|
|
if !strings.HasPrefix(columns[len(columns)-1], "//") {
|
|
|
|
|
|
columns = append(columns, "//")
|
|
|
|
|
|
}
|
|
|
|
|
|
if debug {
|
|
|
|
|
|
// fmt.Printf("columns: %v\n", strings.Join(columns, ","))
|
|
|
|
|
|
// fmt.Printf("columns[len(columns)-2]: %v\n", columns[len(columns)-2])
|
|
|
|
|
|
}
|
|
|
|
|
|
typ := columns[len(columns)-2]
|
|
|
|
|
|
// 处理JSON标签
|
|
|
|
|
|
var jsonName string
|
|
|
|
|
|
for _, v := range columns[:len(columns)-2] {
|
|
|
|
|
|
// 跳过JSON标签和binding标签
|
|
|
|
|
|
if strings.Contains(v, "`json:") || strings.Contains(v, "`binding:") {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取JSON标签名
|
|
|
|
|
|
jsonTagMatch := regexp.MustCompile("`json:\"([^,\"]+)").FindStringSubmatch(line)
|
|
|
|
|
|
if len(jsonTagMatch) > 1 {
|
|
|
|
|
|
jsonName = jsonTagMatch[1]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
jsonName = v
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 忽略binding标签对类型的影响
|
|
|
|
|
|
if strings.Contains(typ, "binding:") {
|
|
|
|
|
|
// 找到正确的类型(通常是binding标签前的类型)
|
|
|
|
|
|
for j, column := range columns {
|
|
|
|
|
|
if j > 0 && strings.Contains(column, "binding:") {
|
|
|
|
|
|
typ = columns[j-1]
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
desc := cloneMap(convertType(typ))
|
|
|
|
|
|
if v := columns[len(columns)-1]; v != "//" {
|
|
|
|
|
|
desc["description"] = strings.TrimPrefix(v, "//")
|
|
|
|
|
|
}
|
|
|
|
|
|
props[jsonName] = desc
|
|
|
|
|
|
|
|
|
|
|
|
if typ == "string" { // 设置 string 默认值为 空串
|
|
|
|
|
|
desc["example"] = ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析 example 的默认值 example:"1"
|
|
|
|
|
|
match := regexp.MustCompile("example:\"([\\w\\d]+)\"").FindStringSubmatch(line)
|
|
|
|
|
|
if len(match) < 2 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
desc["example"] = match[1]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return props
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var baseInfoNames = map[string]bool{
|
|
|
|
|
|
//"UserLabelInfoItem": true,
|
|
|
|
|
|
//"Location": true,
|
|
|
|
|
|
|
|
|
|
|
|
"RedPacket": true,
|
|
|
|
|
|
"HongBaoItem": true,
|
|
|
|
|
|
"HongBaoURLItem": true,
|
|
|
|
|
|
"GetRedPacketList": true,
|
|
|
|
|
|
|
|
|
|
|
|
"TimelineObject": true,
|
|
|
|
|
|
"ActionInfo": true,
|
|
|
|
|
|
"AppInfo": true,
|
|
|
|
|
|
"AppMsg": true,
|
|
|
|
|
|
"ContentObject": true,
|
|
|
|
|
|
"Location": true,
|
|
|
|
|
|
"Media": true,
|
|
|
|
|
|
"MediaList": true,
|
|
|
|
|
|
"StreamVideo": true,
|
|
|
|
|
|
"Enc": true,
|
|
|
|
|
|
"Size": true,
|
|
|
|
|
|
"Thumb": true,
|
|
|
|
|
|
"URL": true,
|
|
|
|
|
|
"VideoSize": true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析 controller 中使用的 项目根目录下 clientsdk/baseinfo 目录内的 Model 结构体
|
|
|
|
|
|
func analyzeBaseInfoStruct(definitions SwgMap) {
|
|
|
|
|
|
dirs, _ := os.ReadDir("./clientsdk/baseinfo")
|
|
|
|
|
|
for _, dir := range dirs {
|
|
|
|
|
|
dataBytes, _ := os.ReadFile(path.Join("./clientsdk/baseinfo", dir.Name()))
|
|
|
|
|
|
lines := strings.Split(string(dataBytes), "\n")
|
|
|
|
|
|
|
|
|
|
|
|
for i := 0; i < len(lines); i++ {
|
|
|
|
|
|
line := lines[i]
|
|
|
|
|
|
if !strings.HasPrefix(line, "type") {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
match := regexp.MustCompile("type ([\\w.]+) struct").FindStringSubmatch(line)
|
|
|
|
|
|
if len(match) < 2 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
name := match[1]
|
|
|
|
|
|
if !baseInfoNames[name] {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
definitions[name] = SwgMap{
|
|
|
|
|
|
"title": name,
|
|
|
|
|
|
"type": "object",
|
|
|
|
|
|
"properties": analyzeModeStruct(lines, i+1, name == "MessageItem"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var typesMap = map[string]SwgMap{
|
|
|
|
|
|
"byte": {"type": "integer", "format": "int"},
|
|
|
|
|
|
"int": {"type": "integer", "format": "int"},
|
|
|
|
|
|
"int32": {"type": "integer", "format": "int32"},
|
|
|
|
|
|
"int64": {"type": "integer", "format": "int64"},
|
|
|
|
|
|
"uint64": {"type": "integer", "format": "uint64"},
|
|
|
|
|
|
"bool": {"type": "boolean"},
|
|
|
|
|
|
"uint32": {"type": "integer", "format": "uint32"},
|
|
|
|
|
|
"float32": {"type": "number", "format": "double"},
|
|
|
|
|
|
"float64": {"type": "number", "format": "double"},
|
|
|
|
|
|
"string": {"type": "string"},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TagInfo 定义结构体来存储索引和名称
|
|
|
|
|
|
type TagInfo struct {
|
|
|
|
|
|
Index int
|
|
|
|
|
|
Name string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建特定的回调标签
|
|
|
|
|
|
var callbackTags = map[string]string{
|
|
|
|
|
|
"/message/SetCallback": "消息回调",
|
|
|
|
|
|
"/message/GetCallback": "消息回调",
|
|
|
|
|
|
"/message/DeleteCallback": "消息回调",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func getTag(key string) string {
|
|
|
|
|
|
v := srvconfig.GlobalSetting.ApiVersion
|
|
|
|
|
|
k := key
|
|
|
|
|
|
if strings.HasPrefix(key, v) {
|
|
|
|
|
|
k = key[len(v):]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否为回调API路径
|
|
|
|
|
|
if tag, exists := callbackTags[k]; exists {
|
|
|
|
|
|
return tag
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if value, ok := tagsMap[k]; ok {
|
|
|
|
|
|
return value.Name
|
|
|
|
|
|
}
|
|
|
|
|
|
return key
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func getValue(key string) int {
|
|
|
|
|
|
// 消息回调路径和标签值为 -100,确保它们排在最前面
|
|
|
|
|
|
// 直接检查路径中是否包含特定的回调API路径
|
|
|
|
|
|
if strings.Contains(key, "/message/SetCallback") ||
|
|
|
|
|
|
strings.Contains(key, "/message/GetCallback") ||
|
|
|
|
|
|
strings.Contains(key, "/message/DeleteCallback") {
|
|
|
|
|
|
return -200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tagsMap := map[string]int{
|
|
|
|
|
|
"/login": -1,
|
|
|
|
|
|
"/message": 0,
|
|
|
|
|
|
"/pay": 1,
|
|
|
|
|
|
"/friend": 2,
|
|
|
|
|
|
"/user": 3,
|
|
|
|
|
|
"/group": 4,
|
|
|
|
|
|
"/admin": 5,
|
|
|
|
|
|
"/label": 6,
|
|
|
|
|
|
"/ws": 7,
|
|
|
|
|
|
"/sns": 8,
|
|
|
|
|
|
"/equipment": 9,
|
|
|
|
|
|
"/applet": 10,
|
|
|
|
|
|
"/favor": 11,
|
|
|
|
|
|
"/finder": 12,
|
|
|
|
|
|
"/qy": 13,
|
|
|
|
|
|
"/other": 14,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for path, val := range tagsMap {
|
|
|
|
|
|
if strings.HasPrefix(key, path) {
|
|
|
|
|
|
return val
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 100
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var tagsMap = map[string]TagInfo{
|
|
|
|
|
|
//"/index/login": {0, "展示登录二维码"},
|
|
|
|
|
|
"/admin": {0, "管理"},
|
|
|
|
|
|
"/login": {1, "登录"},
|
|
|
|
|
|
"/ws": {2, "同步消息"},
|
|
|
|
|
|
"/message": {-1, "消息"},
|
|
|
|
|
|
"/pay": {4, "支付"},
|
|
|
|
|
|
|
|
|
|
|
|
"/friend": {5, "朋友"},
|
|
|
|
|
|
"/user": {6, "用户"},
|
|
|
|
|
|
"/group": {7, "群管理"},
|
|
|
|
|
|
"/label": {8, "标签"},
|
|
|
|
|
|
"/applet": {9, "公众号/小程序"},
|
|
|
|
|
|
"/sns": {10, "朋友圈"},
|
|
|
|
|
|
"/finder": {11, "视频号"},
|
|
|
|
|
|
"/favor": {12, "收藏"},
|
|
|
|
|
|
"/qy": {13, "企业微信"},
|
|
|
|
|
|
"/equipment": {14, "设备"},
|
|
|
|
|
|
"/other": {15, "其他"},
|
|
|
|
|
|
// "/cmd": {16, "用户指令"},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func convertType(typ string) SwgMap {
|
|
|
|
|
|
if strings.HasPrefix(typ, "[]") {
|
|
|
|
|
|
itr := strings.TrimPrefix(typ, "[]")
|
|
|
|
|
|
return SwgMap{"type": "array", "items": convertType(itr)}
|
|
|
|
|
|
}
|
|
|
|
|
|
if v, ok := typesMap[typ]; ok {
|
|
|
|
|
|
return v
|
|
|
|
|
|
}
|
|
|
|
|
|
return SwgMap{"$ref": "#/definitions/" + strings.Trim(typ, "*")}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func listTrimSpace(src []string) []string {
|
|
|
|
|
|
dst := make([]string, 0)
|
|
|
|
|
|
for i, v := range src {
|
|
|
|
|
|
if strings.TrimSpace(v) != "" {
|
|
|
|
|
|
if strings.HasPrefix(v, "`json:") {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.HasPrefix(v, "`xml:") {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.HasPrefix(v, "`example:") {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.HasPrefix(v, "//") {
|
|
|
|
|
|
dst = append(dst, strings.Join(src[i:], " "))
|
|
|
|
|
|
break
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dst = append(dst, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return dst
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func cloneMap(src SwgMap) SwgMap {
|
|
|
|
|
|
dict := make(SwgMap)
|
|
|
|
|
|
for k, v := range src {
|
|
|
|
|
|
dict[k] = v
|
|
|
|
|
|
}
|
|
|
|
|
|
return dict
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func analyseControllers(routers map[string][][]string) SwgMap {
|
|
|
|
|
|
paths := SwgMap{}
|
|
|
|
|
|
handles := map[string]SwgMap{}
|
|
|
|
|
|
dirs, _ := os.ReadDir("./api/controller")
|
|
|
|
|
|
for _, dir := range dirs {
|
|
|
|
|
|
if strings.HasPrefix(dir.Name(), "baseController") {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
byts, _ := os.ReadFile(path.Join("./api/controller", dir.Name()))
|
|
|
|
|
|
lines := strings.Split(string(byts), "\n")
|
|
|
|
|
|
for i, v := range lines {
|
|
|
|
|
|
if strings.Contains(v, "func") && strings.Contains(v, "gin.Context)") {
|
|
|
|
|
|
matches := regexp.MustCompile("func ([\\w\\d]+)\\(c?tx? \\*gin.Context\\)").FindStringSubmatch(v)
|
|
|
|
|
|
if len(matches) < 2 {
|
|
|
|
|
|
// 正则表达式匹配失败,跳过此函数
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
name := matches[1]
|
|
|
|
|
|
annotation := ""
|
|
|
|
|
|
if i > 0 {
|
|
|
|
|
|
annotation = strings.TrimSpace(strings.Trim(lines[i-1], "// "+name+" "))
|
|
|
|
|
|
}
|
|
|
|
|
|
pLine := ""
|
|
|
|
|
|
if i+1 < len(lines) {
|
|
|
|
|
|
pLine = lines[i+1]
|
|
|
|
|
|
if strings.TrimSpace(pLine) == "" && i+2 < len(lines) {
|
|
|
|
|
|
pLine = lines[i+2]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
match := regexp.MustCompile("new\\((?:req|baseinfo).([\\w\\d]+)\\)").FindStringSubmatch(pLine)
|
|
|
|
|
|
params := []SwgMap{
|
|
|
|
|
|
{"in": "query",
|
|
|
|
|
|
"name": "key",
|
|
|
|
|
|
"type": "string",
|
|
|
|
|
|
"description": "账号唯一标识",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(match) > 1 {
|
|
|
|
|
|
params = append(params, SwgMap{
|
|
|
|
|
|
"in": "body",
|
|
|
|
|
|
"name": "body",
|
|
|
|
|
|
"description": "请求参数",
|
|
|
|
|
|
"schema": SwgMap{
|
|
|
|
|
|
"$ref": "#/definitions/" + match[1],
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
handles[name] = SwgMap{
|
|
|
|
|
|
"summary": annotation,
|
|
|
|
|
|
"parameters": params,
|
|
|
|
|
|
"responses": SwgMap{
|
|
|
|
|
|
"200": SwgMap{
|
|
|
|
|
|
"description": "",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for k, list := range routers {
|
|
|
|
|
|
for _, v := range list {
|
|
|
|
|
|
if len(v) < 3 {
|
|
|
|
|
|
// 路由定义不完整,跳过
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
api := fmt.Sprint(k, "/", v[1])
|
|
|
|
|
|
method := strings.ToLower(v[0])
|
|
|
|
|
|
handler := strings.TrimPrefix(v[2], "controller.")
|
|
|
|
|
|
hmap, ok := handles[handler]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
// 找不到对应的处理函数,跳过
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
hmap["tags"] = []string{getTag(k)}
|
|
|
|
|
|
paths[api] = SwgMap{method: hmap}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return paths
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func analyzeGroupRouter(lines []string, pos int) (int, [][]string) {
|
|
|
|
|
|
handles := make([][]string, 0)
|
|
|
|
|
|
for i, v := range lines[pos:] {
|
|
|
|
|
|
line := strings.TrimSpace(v)
|
|
|
|
|
|
if strings.HasPrefix(line, "{") ||
|
|
|
|
|
|
strings.HasPrefix(line, "//") ||
|
|
|
|
|
|
len(line) == 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
} else if strings.HasPrefix(line, "}") {
|
|
|
|
|
|
return pos + i, handles
|
|
|
|
|
|
}
|
|
|
|
|
|
match := regexp.MustCompile("([POSTGE]+)\\(\"/([\\w\\d]+)\", ([\\d\\w.]+)\\)").FindStringSubmatch(line)
|
|
|
|
|
|
if len(match) >= 4 {
|
|
|
|
|
|
handles = append(handles, match[1:])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return len(lines), handles
|
|
|
|
|
|
}
|