// Package mocksrv:本地 Mock 服务(引擎 + 存储 + 限流 + 路由)。 // 引擎与前端 packages/core/src/mock.ts 同语法(@cname / @integer(1,100) / …), // 每次请求重新求值,返回随机数据。 package mocksrv import ( "encoding/json" "fmt" "math/rand" "regexp" "strconv" "strings" "time" ) var ( surname = []rune("赵钱孙李周吴郑王冯陈蒋沈韩杨朱秦许何吕张孔曹严华金魏陶姜谢邹柏水窦章云苏潘葛范彭鲁马苗凤花方任袁柳史唐薛雷贺倪汤") given = []rune("伟芳娜秀英敏静丽强磊军洋勇艳杰娟涛明超秀兰霞平刚华民玉萍红娥玲芬芳燕春菊学成嘉昊泽宇欣怡思远志坚建国子涵梓萱浩然雨桐") provinces = []string{"北京市", "上海市", "天津市", "重庆市", "河北省", "山西省", "辽宁省", "吉林省", "黑龙江省", "江苏省", "浙江省", "安徽省", "福建省", "江西省", "山东省", "河南省", "湖北省", "湖南省", "广东省", "海南省", "四川省", "贵州省", "云南省", "陕西省", "甘肃省", "青海省", "台湾省", "内蒙古自治区", "广西壮族自治区", "西藏自治区", "宁夏回族自治区", "新疆维吾尔自治区", "香港特别行政区", "澳门特别行政区"} cities = []string{"广州", "深圳", "杭州", "成都", "武汉", "西安", "南京", "苏州", "长沙", "郑州", "青岛", "大连", "厦门", "宁波", "无锡", "佛山", "东莞", "沈阳", "哈尔滨", "石家庄", "太原", "合肥", "福州", "南昌", "济南", "昆明", "贵阳", "兰州", "西宁", "南宁", "呼和浩特", "乌鲁木齐"} counties = []string{"朝阳区", "海淀区", "浦东新区", "南山区", "福田区", "天河区", "西湖区", "锦江区", "雁塔区", "玄武区", "工业园区", "香洲区", "东湖区", "岳麓区", "金水区", "市南区", "和平区", "河东区"} letters = "abcdefghijklmnopqrstuvwxyz" uppers = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" digits = "0123456789" phoneHeads = []string{"130", "131", "132", "133", "135", "136", "137", "138", "139", "150", "151", "152", "155", "156", "157", "158", "159", "170", "176", "177", "178", "180", "181", "182", "183", "185", "186", "187", "188", "189", "199"} ) func pickStr(list []string) string { return list[rand.Intn(len(list))] } func randFrom(pool string, n int) string { b := make([]byte, n) for i := range b { b[i] = pool[rand.Intn(len(pool))] } return string(b) } func randInt(min, max int) int { return min + rand.Intn(max-min+1) } func randRunes(pool []rune, n int) string { b := make([]rune, n) for i := range b { b[i] = pool[rand.Intn(len(pool))] } return string(b) } var exprRe = regexp.MustCompile(`^@([a-zA-Z][\w-]*)(?:\((.*)\))?$`) func parseArgs(raw string) []string { if strings.TrimSpace(raw) == "" { return nil } var out []string var cur strings.Builder quote := byte(0) for i := 0; i < len(raw); i++ { ch := raw[i] if quote != 0 { if ch == quote { quote = 0 } else { cur.WriteByte(ch) } continue } if ch == '"' || ch == '\'' { quote = ch continue } if ch == ',' { out = append(out, strings.TrimSpace(cur.String())) cur.Reset() continue } cur.WriteByte(ch) } if s := strings.TrimSpace(cur.String()); s != "" { out = append(out, s) } return out } func argInt(args []string, i, def int) int { if i >= len(args) { return def } n, err := strconv.Atoi(args[i]) if err != nil { return def } return n } func formatDate(layout string, t time.Time) string { r := strings.NewReplacer( "YYYY", "2006", "YY", "06", "MM", "01", "DD", "02", "HH", "15", "mm", "04", "ss", "05", ) return t.Format(r.Replace(layout)) } // Generate 求 Mock 表达式;非 @ 开头原样返回。 func Generate(expr string) string { expr = strings.TrimSpace(expr) m := exprRe.FindStringSubmatch(expr) if m == nil { return expr } name := strings.ToLower(m[1]) args := parseArgs(m[2]) now := time.Now() switch name { case "integer": return strconv.Itoa(randInt(argInt(args, 0, 0), argInt(args, 1, 100))) case "natural": return strconv.Itoa(randInt(argInt(args, 0, 1), argInt(args, 1, 1000))) case "float": min := float64(argInt(args, 0, 0)) max := float64(argInt(args, 1, 100)) d := argInt(args, 2, 2) return strconv.FormatFloat(min+rand.Float64()*(max-min), 'f', d, 64) case "timestamp": return strconv.FormatInt(now.UnixMilli(), 10) case "boolean", "bool": return strconv.FormatBool(rand.Intn(2) == 0) case "pick": if len(args) == 0 { return "A" } return args[rand.Intn(len(args))] case "string": pool := letters + uppers + digits min, max := 5, 10 if len(args) > 0 { switch args[0] { case "lower": pool = letters case "upper": pool = uppers case "number": pool = digits } min, max = argInt(args, 1, 5), argInt(args, 2, argInt(args, 1, 5)) } else { min, max = argInt(args, 0, 5), argInt(args, 1, argInt(args, 0, 5)) } return randFrom(pool, randInt(min, max)) case "character": return randFrom(letters+uppers+digits, 1) case "word", "cword": return randRunes(given, randInt(argInt(args, 0, 3), argInt(args, 1, argInt(args, 0, 3)))) case "cname": return string(surname[rand.Intn(len(surname))]) + randRunes(given, randInt(1, 2)) case "cfirst": return string(surname[rand.Intn(len(surname))]) case "clast": return randRunes(given, randInt(1, 2)) case "email": domains := []string{"qq.com", "163.com", "gmail.com", "outlook.com", "example.com"} return fmt.Sprintf("%s%d@%s", randFrom(letters, randInt(4, 9)), randInt(1, 99), pickStr(domains)) case "url": hosts := []string{"www.example.com", "api.example.com", "cdn.example.com", "example.org"} schemes := []string{"https", "http"} return fmt.Sprintf("%s://%s/%s", pickStr(schemes), pickStr(hosts), randFrom(letters, randInt(3, 8))) case "domain": tlds := []string{"com", "cn", "net", "org", "io"} return fmt.Sprintf("%s.%s", randFrom(letters, randInt(4, 10)), pickStr(tlds)) case "ip": return fmt.Sprintf("%d.%d.%d.%d", randInt(1, 254), randInt(0, 255), randInt(0, 255), randInt(1, 254)) case "guid", "uuid": hexs := "0123456789abcdef" seg := func(n int) string { return randFrom(hexs, n) } return fmt.Sprintf("%s-%s-%s-%s-%s", seg(8), seg(4), seg(4), seg(4), seg(12)) case "id": return fmt.Sprintf("%d%02d%02d%02d%03d%d", randInt(110000, 659000), randInt(1960, 2005), randInt(1, 12), randInt(1, 28), randInt(100, 999), randInt(0, 9)) case "phone", "mobile", "tel": return pickStr(phoneHeads) + randFrom(digits, 8) case "date": return formatDate(argLayout(args, "YYYY-MM-DD"), now) case "time": return formatDate(argLayout(args, "HH:mm:ss"), now) case "datetime", "now": return formatDate(argLayout(args, "YYYY-MM-DD HH:mm:ss"), now) case "province": return pickStr(provinces) case "city": prefix := "" if len(args) > 0 && args[0] == "true" { prefix = pickStr(provinces) } return prefix + pickStr(cities) + "市" case "county": prefix := "" if len(args) > 0 && args[0] == "true" { prefix = pickStr(provinces) + pickStr(cities) + "市" } return prefix + pickStr(counties) case "zip", "zipcode": return randFrom(digits, 6) case "ctitle", "ctitlex": return randRunes(given, randInt(argInt(args, 0, 4), argInt(args, 1, 8))) case "csentence": return randRunes(given, randInt(argInt(args, 0, 8), argInt(args, 1, 16))) + "。" case "cparagraph": n := argInt(args, 0, 2) var sb strings.Builder for i := 0; i < n; i++ { sb.WriteString(randRunes(given, randInt(10, 20))) sb.WriteString("。") } return sb.String() case "image": size := "480x480" if len(args) > 0 && regexp.MustCompile(`^\d+x\d+$`).MatchString(args[0]) { size = args[0] } else { size = fmt.Sprintf("%dx%d", randInt(2, 8)*100, randInt(2, 8)*100) } seed := randFrom(letters+digits, 8) return fmt.Sprintf("https://picsum.photos/seed/%s/%s", seed, strings.ReplaceAll(size, "x", "/")) default: return expr } } func argLayout(args []string, def string) string { if len(args) > 0 && strings.ContainsAny(args[0], "YMDHms") { return args[0] } return def } // GenerateJSON:对 JSON 模板的叶子 @表达式逐请求生成;解析失败原样返回。 func GenerateJSON(template string) string { trimmed := strings.TrimSpace(template) if trimmed == "" { return template } if m := exprRe.FindStringSubmatch(trimmed); m != nil { return Generate(trimmed) // 顶层就是表达式 } var root interface{} if err := json.Unmarshal([]byte(trimmed), &root); err != nil { return template } out := walkGenerate(root) b, err := json.MarshalIndent(out, "", " ") if err != nil { return template } return string(b) } func walkGenerate(node interface{}) interface{} { switch v := node.(type) { case map[string]interface{}: out := make(map[string]interface{}, len(v)) for k, val := range v { out[k] = walkGenerate(val) } return out case []interface{}: out := make([]interface{}, len(v)) for i, val := range v { out[i] = walkGenerate(val) } return out case string: s := strings.TrimSpace(v) if strings.HasPrefix(s, "@") && exprRe.MatchString(s) { raw := Generate(s) // 数字/布尔叶子保持类型 if n, err := strconv.Atoi(raw); err == nil { return n } if f, err := strconv.ParseFloat(raw, 64); err == nil && strings.Contains(raw, ".") { return f } if raw == "true" || raw == "false" { return raw == "true" } return raw } return v default: return node } }