This commit is contained in:
oneao committed 2025-11-06 17:32:51 +08:00
1 parent 6849579652
commit d60be4a75b
24 files changed
+463 -179

No files matched your search

@@ -12,5 +12,6 @@ export default antfu({
'no-console': 'off', // 关闭 console 报错
'node/prefer-global/process': 'off',
'@typescript-eslint/no-require-imports': 'off',
'no-useless-catch': 'off',
},
})
+5 -15
View File
@@ -5,7 +5,7 @@ import { Stack, useRouter } from 'expo-router'
import * as SplashScreen from 'expo-splash-screen'
import { StatusBar } from 'expo-status-bar'
import { RefreshCw } from 'lucide-react-native'
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import EmptyState from '@/components/common/EmptyStatus'
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'
import { useUserStore } from '@/store/user'
@@ -33,33 +33,23 @@ export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
export default function RootLayout() {
const router = useRouter()
const token = useUserStore(state => state.token)
const userStore = useUserStore()
const [fontsLoaded] = useFonts({
SpaceMono: require('@/static/fonts/SpaceMono-Regular.ttf'),
})
const [storeLoaded, setStoreLoaded] = useState(false)
// 等待 store 恢复数据
useEffect(() => {
if (token !== undefined) {
setStoreLoaded(true)
}
}, [token])
// splash screen 隐藏 & 登录跳转
useEffect(() => {
if (fontsLoaded && storeLoaded) {
if (fontsLoaded) {
SplashScreen.hideAsync()
// 如果没有 token,则跳转登录页
if (token === null) {
if (!userStore.isLoggedIn()) {
router.replace('/auth')
}
}
}, [fontsLoaded, storeLoaded, token])
}, [fontsLoaded])
return (
<GluestackUIProvider>
@@ -1,6 +1,9 @@
import type { StepLayoutItem } from '@/components/custom/StepLayout'
import { Slot } from 'expo-router'
import { Slot, Stack, useRouter } from 'expo-router'
import { Image, Text, View } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import StepLayout from '@/components/custom/StepLayout'
import CustomTabbar from '@/components/custom/Tabbar'
const steps: StepLayoutItem[] = [
{
@@ -22,5 +25,6 @@ export default function AuthLayout() {
<StepLayout steps={steps}>
<Slot />
</StepLayout>
)
}
@@ -36,17 +36,20 @@ export default function AuthLogin() {
setForm(prev => ({ ...prev, [field]: value }))
}
const defaultLogin = () => {
alovaInstance.Post('/auth/login/default', {
alovaInstance.Post('/auth/test', {
title: 'foo',
body: 'bar',
userId: 1,
}).then((res) => {
console.log(res)
})
.then((res) => {
console.log('成功返回:', res)
})
.catch((err) => {
console.log('请求失败或未授权:', err)
})
}
return (
<View className="flex flex-col items-center px-6 h-full">
<Image
source={require('@/static/images/logo.png')}
@@ -42,7 +42,7 @@ export default function AuthRegister() {
return (
<ScrollView ref={scrollRef} style={{ flex: 1 }}>
<View className="flex flex-col items-center px-6">
<View className="flex flex-col items-center px-6 bg-white ">
<Image
source={require('@/static/images/logo.png')}
style={{ width: 120, height: 120 }}
@@ -1,7 +1,7 @@
import { Ionicons } from '@expo/vector-icons'
import { usePathname, useRouter } from 'expo-router'
import { Image, Pressable, ScrollView, Text, View } from 'react-native'
import Animated, { SlideInRight } from 'react-native-reanimated'
import { Image, Pressable, Text, View } from 'react-native'
import Animated, { SlideInLeft, SlideInRight } from 'react-native-reanimated'
import { SafeAreaView } from 'react-native-safe-area-context'
export interface StepLayoutItem {
@@ -31,11 +31,13 @@ export default function StepLayout({ steps, children, showHearder = true }: Step
// 是否显示header
const headerVisible = showHearder !== false && currentStep.showHearder !== false
const enteringAnimation = canGoBack ? SlideInRight.duration(300) : SlideInLeft.duration(300)
return (
<SafeAreaView style={{ flex: 1 }}>
<Animated.View
key={pathname}
entering={SlideInRight.duration(300)}
entering={enteringAnimation}
style={{ flex: 1 }}
>
{headerVisible && (
@@ -44,10 +46,7 @@ export default function StepLayout({ steps, children, showHearder = true }: Step
onPress={canGoBack ? (currentStep.onBack ?? (() => router.back())) : undefined}
style={{ opacity: canGoBack ? 1 : 0 }}
>
<Ionicons
name="arrow-back"
size={22}
/>
<Ionicons name="arrow-back" size={22} />
</Pressable>
<Text className="flex-1 text-center text-lg font-semibold">
Whitespace-only changes.
+101 -12
View File
@@ -1,21 +1,110 @@
import type { Method } from 'alova'
import type { AxiosError } from 'axios'
import { axiosRequestAdapter } from '@alova/adapter-axios'
import { createAlova } from 'alova'
import { createServerTokenAuthentication } from 'alova/client'
import adapterFetch from 'alova/fetch'
import { useToast } from '@/utils/toast'
import { router } from 'expo-router'
import { userStore } from '@/store/user'
import { toast } from '@/utils/toast'
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({
// ...
})
enum HttpCode {
Success = 200,
Unauthorized = 401,
RefreshToken = 402,
Fail = 500,
}
const WhiteList = [
'/auth/login/**',
'/auth/register/**',
'/auth/refresh/token',
]
function isWhiteListed(url: string) {
return WhiteList.some((pattern) => {
if (pattern.endsWith('/**')) {
const base = pattern.slice(0, -3)
return url.startsWith(base)
}
return url === pattern
})
}
const alovaInstance = createAlova({
// baseURL: 'http://192.168.1.136:8085/app',
baseURL: 'http://192.168.5.6:8085/app',
baseURL: 'http://192.168.1.136:8085/app',
requestAdapter: axiosRequestAdapter(),
responded(response) {
// response自动被推断为AxiosResponse类型
return response.data
responded: {
onSuccess: async (response, method: Method) => {
const resData = response.data as ResponseData<any>
if (resData.code === HttpCode.Success) {
return resData.data
}
// Access Token 失效 → 重新登录
if (resData.code === HttpCode.Unauthorized) {
console.log('===到这一步了===')
router.replace('/auth/login')
return
}
// Access Token 过期但 Refresh Token 有效 → 刷新 token 并重试
if (resData.code === HttpCode.RefreshToken) {
try {
const refreshToken = userStore.getState().userInfo?.refreshToken
if (!refreshToken) {
router.replace('/auth/login')
return
}
// 调用刷新接口
const res = await alovaInstance.Get<ResponseData<string>>('/auth/refresh/token', {
headers: {
refreshToken,
},
})
const newToken = res.data
// 更新 store
userStore.getState().setUserInfo({
...userStore.getState().userInfo!,
token: newToken,
})
// 重试原请求
method.config.headers.token = newToken
await method.send()
return
}
catch (err) {
console.log('===到这一步了===')
router.replace('/auth/login')
return Promise.reject(err)
}
}
// 其他错误
return Promise.reject(resData.message || '请求失败')
},
onError: (error: AxiosError, method) => {
console.log(error.status)
return Promise.reject(error)
},
},
beforeRequest(method) {
if (isWhiteListed(method.url))
return
if (!userStore.getState().isLoggedIn()) {
toast.show({
title: '尚未登录',
})
router.replace('/auth/login')
return
}
method.config.headers.token = userStore.getState().userInfo?.token ?? ''
},
})
+9 -10
View File
@@ -4,26 +4,22 @@ import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'
interface UserState {
userInfo: { userId: string, username: string } | null
token: string | null
setUserInfo: (userInfo: UserState['userInfo']) => void
setToken: (token: string) => void
userInfo: UserInfo | null
setUserInfo: (userInfo: UserInfo | null) => void
logout: () => void
isLoggedIn: () => boolean
}
export const useUserStore = create<UserState>()(
const useUserStore = create<UserState>()(
persist(
(set, get) => ({
userInfo: null,
token: null,
setUserInfo: userInfo => set({ userInfo }),
setToken: token => set({ token }),
logout: () => {
console.log('当前 token:', get().token)
set({ userInfo: null, token: null })
console.log('当前 token:', get().userInfo?.token)
set({ userInfo: null })
},
isLoggedIn: () => !!get().token,
isLoggedIn: () => !!get().userInfo?.token,
}),
{
name: 'user-storage',
@@ -31,3 +27,6 @@ export const useUserStore = create<UserState>()(
},
),
)
const userStore = useUserStore
export { userStore, useUserStore }
+20
View File
@@ -0,0 +1,20 @@
export {}
declare global {
interface ResponseData<T> {
trackId: string
code: number
message: string
data: T
}
interface UserInfo {
account: string
id: string
seqNo: number
username: string
gender: number
token?: string
refreshToken?: string
}
}
+81 -13
View File
@@ -1,5 +1,5 @@
import type { IToastProps } from '@gluestack-ui/core/lib/esm/toast/creator/types'
import React, { useRef } from 'react'
import React, { createContext, useContext, useRef } from 'react'
import { Toast, ToastDescription, ToastTitle, useToast as useGluestackToast } from '@/components/ui/toast'
type ToastVariant = 'solid' | 'outline'
@@ -12,28 +12,74 @@ interface ShowToastOptions extends IToastProps {
action?: ToastAction
}
// 创建全局 toast 实例
let globalToast: any = null
// 创建 Context 用于在组件内部使用
const ToastContext = createContext<ReturnType<typeof useGluestackToast> | null>(null)
export function ToastProviderWrapper({ children }: { children: React.ReactNode }) {
const toast = useGluestackToast()
const toastRef = useRef(toast)
toastRef.current = toast
return <>{children}</>
// 设置全局实例
React.useEffect(() => {
globalToast = {
show: (options: ShowToastOptions) => {
const id = Math.random().toString()
toast.show({
id,
placement: options.placement ?? 'top',
duration: options.duration !== undefined ? options.duration : 3000,
avoidKeyboard: options.avoidKeyboard ?? false,
containerStyle: options.containerStyle,
onCloseComplete: options.onCloseComplete,
render: options.render
? () => options.render!({ id })
: ({ id }) => (
<Toast
nativeID={`toast-${id}`}
variant={options.variant ?? 'solid'}
action={options.action ?? 'muted'}
>
{options.title && <ToastTitle className="font-blod">{options.title}</ToastTitle>}
{options.description && <ToastDescription>{options.description}</ToastDescription>}
</Toast>
),
})
return id
},
close: (id: string) => {
toast.close(id)
},
closeAll: () => {
toast.closeAll()
},
isActive: (id: string) => toast.isActive(id),
}
}, [toast])
return (
<ToastContext.Provider value={toast}>
{children}
</ToastContext.Provider>
)
}
/**
* 自定义 Hook,用法:
* const toast = useToast();
* toast.show({ ... });
* toast.close(id);
* toast.closeAll();
* 自定义 Hook,用于 React 组件内部
*/
export function useToast() {
const toast = useGluestackToast()
const toast = useContext(ToastContext)
if (!toast) {
throw new Error('useToast must be used within a ToastProviderWrapper')
}
return {
show: (options: ShowToastOptions) => {
const id = Math.random().toString()
toast.show({
id,
placement: options.placement ?? 'top',
@@ -54,18 +100,40 @@ export function useToast() {
</Toast>
),
})
return id
},
close: (id: string) => {
toast.close(id)
},
closeAll: () => {
toast.closeAll()
},
isActive: (id: string) => toast.isActive(id),
}
}
/**
* 全局 toast 实例,可在组件外部使用
*/
export const toast = {
show: (options: ShowToastOptions) => {
if (!globalToast) {
console.warn('Toast not initialized. Make sure ToastProviderWrapper is rendered.')
return ''
}
return globalToast.show(options)
},
close: (id: string) => {
if (globalToast) {
globalToast.close(id)
}
},
closeAll: () => {
if (globalToast) {
globalToast.closeAll()
}
},
isActive: (id: string) => {
return globalToast ? globalToast.isActive(id) : false
},
}
+1 -1
View File
@@ -12,7 +12,7 @@
"include": [
"./**/*.ts",
"./**/*.tsx",
"./src/types/*.d.ts",
"src/**/*.d.ts",
"nativewind-env.d.ts"
],
"exclude": [
@@ -13,18 +13,48 @@ func InitAuthRouter(r *router.Router) {
group := r.Group("auth")
group.POST("/login/default", loginDefault)
group.POST("/register/default", registerDefault)
group.POST("/refresh/token", RefreshAccessToken)
}
//func RefreshToken(c *router.Context) error {
// _, err := c.GetBodyWithRequired("accessToken", "refreshToken")
// if err != nil {
// return err
// }
// //accessToken := bodyData.GetString("accessToken")
// //refreshToken := bodyData.GetString("refreshToken")
//}
func RefreshAccessToken(c *router.Context) error {
// 从 Header 获取 Refresh Token
refreshToken := c.Header("refreshToken")
if refreshToken == "" {
return response.Fail(c).
Code(response.HttpCode.Unauthorized).
Message("未提供 Token").
Send()
}
// 验证 Refresh Token
tokenResult := jwtx.VerifyToken(refreshToken)
if !tokenResult.IsValid || tokenResult.IsExpired {
// Refresh Token 无效或过期,需要重新登录
return response.Fail(c).
Code(response.HttpCode.Unauthorized).
Message("登录已过期,请重新登录").
Send()
}
userID := tokenResult.Claims.UserID
// 生成新的 Access Token
newAccessToken, err := jwtx.CreateAccessToken(userID)
if err != nil {
return err
}
// 返回新的 Access Token
respData := map[string]string{
"accessToken": newAccessToken,
}
return response.Success(c).
Data(respData).
Send()
}
// 默认登录
// 默认登录
func loginDefault(c *router.Context) error {
// 获取请求体并校验必填字段
@@ -43,13 +73,12 @@ func loginDefault(c *router.Context) error {
)
user, err := db.FindOne("user_info", userQuery)
if err != nil {
return response.Success(c).Message("查询用户失败,请稍后重试").IsSuccess(false).Send()
return response.Success(c).Message("查询用户失败,请稍后重试").Send()
}
if user == nil {
// 账号或密码错误
return response.Success(c).
IsSuccess(false).
return response.Fail(c).
Message("账号或密码错误").
Send()
}
@@ -99,7 +128,7 @@ func registerDefault(c *router.Context) error {
}
if len(existingUsers) > 0 {
return response.Error(c).IsSuccess(false).Message("该账号已被注册").Send()
return response.Fail(c).Message("该账号已被注册").Send()
}
// 构造新用户数据
@@ -115,7 +144,7 @@ func registerDefault(c *router.Context) error {
return err
}
if insertedRows != 1 {
return response.Success(c).IsSuccess(false).Message("注册失败").Send()
return response.Fail(c).Message("注册失败").Send()
}
return response.Success(c).Message("注册成功").Send()
@@ -0,0 +1,39 @@
package middleware
import (
response "base-go-v2/internal/reponse"
"base-go-v2/internal/router"
"base-go-v2/internal/utils/jwtx"
)
func AuthMiddleware() router.HandlerFunc {
return func(c *router.Context) error {
token := c.Header("token")
if token == "" {
c.Abort()
return response.Fail(c).
Code(response.HttpCode.Unauthorized).
Message("未提供 Token").
Send()
}
tokenVerify := jwtx.VerifyToken(token)
// Access Token 有效
if tokenVerify.IsValid {
c.Set("user_id", tokenVerify.Claims.UserID)
return nil
}
// Access Token 失效 → 返回刷新提示
if !tokenVerify.IsValid {
c.Abort()
return response.Fail(c).
Code(response.HttpCode.RefreshToken).
Message("Access Token 已过期,请刷新").
Send()
}
return c.Next()
}
}
@@ -28,7 +28,7 @@ func ErrorMiddleware() router.HandlerFunc {
// be 是 nil 指针类型
if errors.As(err, &be) {
// 业务错误,不记录日志
return response.Error(c).Message(err.Error()).Send()
return response.Fail(c).Message(err.Error()).Send()
}
}
@@ -40,6 +40,6 @@ func ErrorMiddleware() router.HandlerFunc {
logx.String("error", err.Error()),
logx.String("stack", stack),
)
return response.Error(c).Message("服务器内部错误").Send()
return response.Fail(c).Message("服务器内部错误").Send()
}
}
@@ -21,7 +21,7 @@ func RecoveryMiddleware() router.HandlerFunc {
)
// 返回统一 JSON 响应
_ = response.Error(c).Message("系统 Panic").Send()
_ = response.Fail(c).Message("系统 Panic").Send()
}
}()
@@ -14,6 +14,7 @@ func InitAppRouter() *router.Router {
r.Use(middleware.CORSMiddleware())
r.Use(middleware.LoggerMiddleware())
r.Use(middleware.ErrorMiddleware())
r.Use(middleware.AuthMiddleware()).ExcludePaths("/auth/**")
auth.InitAuthRouter(r)
return r
@@ -8,128 +8,71 @@ import (
"net/http"
)
// Result 是最终响应结构体
type Result struct {
IsSuccess bool `json:"isSuccess"` // 是否成功
Code string `json:"code"` // 状态码
Message string `json:"message"` // 提示信息
Data interface{} `json:"data,omitempty"` // 数据
TrackId string `json:"trackId,omitempty"` // 请求追踪ID
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
TrackId string `json:"trackId,omitempty"`
}
// Code 定义状态码
type Code struct {
Value string
Message string
var HttpCode = struct {
Success int
Unauthorized int
Fail int
RefreshToken int
}{
Success: 200,
Unauthorized: 401,
Fail: 500,
RefreshToken: 402,
}
// 常用状态码
var (
CodeSuccess = Code{"0000", "请求成功"}
CodeFail = Code{"0001", "请求失败"}
CodeNoLogin = Code{"1001", "未登录"}
CodeLoginExpired = Code{"1002", "登录已过期"}
CodeInvalidToken = Code{"1003", "Token 无效"}
CodeInvalidAccount = Code{"1005", "账号或密码错误"}
)
const defaultMessage = "未知错误"
// Builder 用于构建响应
type Builder struct {
c *router.Context
result Result
}
// ---------------- 构造函数 ----------------
// Success 创建成功响应
func Success(c *router.Context) *Builder {
return &Builder{
c: c,
result: Result{
IsSuccess: true,
Code: CodeSuccess.Value,
Message: CodeSuccess.Message,
Code: HttpCode.Success,
Message: "请求成功",
},
}
}
// Error 创建失败响应
func Error(c *router.Context) *Builder {
func Fail(c *router.Context) *Builder {
return &Builder{
c: c,
result: Result{
IsSuccess: false,
Code: CodeFail.Value,
Message: CodeFail.Message,
Code: HttpCode.Fail,
Message: "请求失败",
},
}
}
// ---------------- 链式方法 ----------------
// 设置 IsSuccess 字段
func (b *Builder) IsSuccess(IsSuccess bool) *Builder {
b.result.IsSuccess = IsSuccess
return b
}
// 设置 Code,并根据 Code 自动设置 IsSuccess
func (b *Builder) Code(code string) *Builder {
func (b *Builder) Code(code int) *Builder {
b.result.Code = code
b.result.Message = GetMessage(code)
b.result.IsSuccess = code == CodeSuccess.Value
return b
}
// 设置 Code 对象,并自动设置 IsSuccess
func (b *Builder) CodeObj(code Code) *Builder {
b.result.Code = code.Value
b.result.Message = code.Message
b.result.IsSuccess = code.Value == CodeSuccess.Value
return b
}
// 自定义提示信息
func (b *Builder) Message(msg string) *Builder {
b.result.Message = msg
return b
}
// 设置返回数据
func (b *Builder) Data(data interface{}) *Builder {
b.result.Data = data
return b
}
// 发送响应
func (b *Builder) Send() error {
// 默认 Data 为空
if b.result.Data == nil {
b.result.Data = ""
}
// 从 routinex 获取 trackId
trackId := routinex.Get(logx.TrackID)
if trackId != nil {
if trackId := routinex.Get(logx.TrackID); trackId != nil {
b.result.TrackId = strutil.ToString(trackId)
}
return b.c.JSON(http.StatusOK, b.result)
}
// ---------------- 辅助函数 ----------------
// 根据 Code 获取默认提示信息
func GetMessage(value string) string {
for _, c := range []Code{
CodeSuccess, CodeFail, CodeNoLogin,
CodeLoginExpired, CodeInvalidToken, CodeInvalidAccount,
} {
if c.Value == value {
return c.Message
}
}
return defaultMessage
}
@@ -67,8 +67,24 @@ func (n *node) search(parts []string, height int, params map[string]string) *nod
// 中间件条目
type middlewareEntry struct {
handler HandlerFunc
excludePaths map[string]struct{}
handler HandlerFunc
excludePaths map[string]struct{}
excludePrefix []string
}
// 判断路径是否被排除
func (m *middlewareEntry) IsExcluded(path string) bool {
if m.excludePaths != nil {
if _, ok := m.excludePaths[path]; ok {
return true
}
}
for _, prefix := range m.excludePrefix {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
// Router 主结构
@@ -78,7 +94,7 @@ type Router struct {
basePath string
}
// NewRouter 创建
// 创建 Router
func NewRouter(basePath ...string) *Router {
path := "/"
if len(basePath) > 0 && basePath[0] != "" {
@@ -90,11 +106,13 @@ func NewRouter(basePath ...string) *Router {
}
}
// MiddlewareHandle 用于链式排除路径
type MiddlewareHandle struct {
router *Router
entryIdx int
}
// Use 注册中间件
func (r *Router) Use(handler HandlerFunc) *MiddlewareHandle {
entry := middlewareEntry{handler: handler}
r.middleware = append(r.middleware, entry)
@@ -104,15 +122,27 @@ func (r *Router) Use(handler HandlerFunc) *MiddlewareHandle {
}
}
// ExcludePaths 支持 /** 前缀匹配
func (mh *MiddlewareHandle) ExcludePaths(paths ...string) *Router {
excludeMap := make(map[string]struct{}, len(paths))
excludeMap := make(map[string]struct{}, 0)
excludePrefix := make([]string, 0)
for _, p := range paths {
excludeMap[p] = struct{}{}
if strings.HasSuffix(p, "/**") {
base := strings.TrimSuffix(p, "/**")
excludePrefix = append(excludePrefix, base)
} else {
excludeMap[p] = struct{}{}
}
}
mh.router.middleware[mh.entryIdx].excludePaths = excludeMap
mh.router.middleware[mh.entryIdx].excludePrefix = excludePrefix
return mh.router
}
// Group 创建子路由组
func (r *Router) Group(prefix string, m ...HandlerFunc) *Router {
newMiddleware := make([]middlewareEntry, len(r.middleware))
copy(newMiddleware, r.middleware)
@@ -127,6 +157,7 @@ func (r *Router) Group(prefix string, m ...HandlerFunc) *Router {
}
}
// GET/POST
func (r *Router) GET(path string, handler HandlerFunc) *Router {
return r.handle("GET", path, handler)
}
@@ -145,7 +176,7 @@ func (r *Router) handle(method, path string, handler HandlerFunc) *Router {
return r
}
// Handler 入口函数(fasthttp)
// Handler 入口(fasthttp)
func (r *Router) Handler(ctx *fasthttp.RequestCtx) {
method := string(ctx.Method())
root := r.roots[method]
@@ -166,10 +197,8 @@ func (r *Router) Handler(ctx *fasthttp.RequestCtx) {
handlers := make([]HandlerFunc, 0)
path := string(ctx.Path())
for _, m := range r.middleware {
if m.excludePaths != nil {
if _, excluded := m.excludePaths[path]; excluded {
continue
}
if m.IsExcluded(path) {
continue
}
handlers = append(handlers, m.handler)
}
@@ -185,6 +214,7 @@ func (r *Router) Handler(ctx *fasthttp.RequestCtx) {
_ = c.Next()
}
// 辅助函数
func parsePattern(pattern string) []string {
vs := strings.Split(strings.Trim(pattern, "/"), "/")
parts := make([]string, 0, len(vs))
@@ -15,9 +15,9 @@ type CustomClaims struct {
// TokenResult 用于返回验证结果
type TokenResult struct {
Claims *CustomClaims
Valid bool
Expired bool
Claims *CustomClaims
IsValid bool
IsExpired bool
}
// CreateAccessToken 创建 Access Token,只返回 token 和 error
@@ -61,7 +61,7 @@ func createToken(userID int, expiry time.Duration) (string, error) {
// VerifyToken 验证 token 并返回结构化结果
func VerifyToken(tokenString string) TokenResult {
result := TokenResult{Valid: false}
result := TokenResult{IsValid: false, IsExpired: false}
token, _ := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(config.App.JWT.Secret), nil
@@ -73,9 +73,9 @@ func VerifyToken(tokenString string) TokenResult {
}
result.Claims = claims
result.Valid = true
result.IsValid = true
if claims.ExpiresAt != nil && time.Now().After(claims.ExpiresAt.Time) {
result.Expired = true
result.IsExpired = true
}
return result
@@ -36,25 +36,25 @@ func NotEmpty(data map[string]interface{}, fields ...string) error {
// collectErrors 递归收集字段错误
func collectErrors(v interface{}, path string, errs map[string]string) {
if v == nil {
errs[path] = fmt.Sprintf("字段 %s 不能为空", path)
errs[path] = "字段不能为空"
return
}
switch val := v.(type) {
case string:
if strings.TrimSpace(val) == "" {
errs[path] = fmt.Sprintf("字段 %s 不能为空", path)
errs[path] = "字段不能为空"
}
case []interface{}:
if len(val) == 0 {
errs[path] = fmt.Sprintf("字段 %s 不能为空", path)
errs[path] = "字段不能为空"
}
for i, elem := range val {
collectErrors(elem, fmt.Sprintf("%s[%d]", path, i), errs)
}
case map[string]interface{}:
if len(val) == 0 {
errs[path] = fmt.Sprintf("字段 %s 不能为空", path)
errs[path] = "字段不能为空"
}
for k, elem := range val {
collectErrors(elem, fmt.Sprintf("%s.%s", path, k), errs)
Whitespace-only changes.
@@ -0,0 +1,69 @@
{"level":"INFO","ts":"2025-11-06 13:11:55.116","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0005349,"trackId":"376961877109899264"}
{"level":"INFO","ts":"2025-11-06 13:14:17.628","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0005502,"trackId":"376962474844356608"}
{"level":"INFO","ts":"2025-11-06 13:15:48.248","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0009768,"trackId":"376962854932185088"}
{"level":"INFO","ts":"2025-11-06 13:19:48.605","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0005207,"trackId":"376963863062511616"}
{"level":"INFO","ts":"2025-11-06 13:21:27.160","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0005053,"trackId":"376964276432142336"}
{"level":"INFO","ts":"2025-11-06 13:22:45.690","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376964605815029760"}
{"level":"INFO","ts":"2025-11-06 13:24:24.423","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376965019931246592"}
{"level":"INFO","ts":"2025-11-06 13:24:44.251","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376965103095906304"}
{"level":"INFO","ts":"2025-11-06 13:26:19.417","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0006208,"trackId":"376965502251040768"}
{"level":"INFO","ts":"2025-11-06 13:31:17.803","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376966753772634112"}
{"level":"INFO","ts":"2025-11-06 13:31:49.685","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376966887495434240"}
{"level":"INFO","ts":"2025-11-06 13:31:51.226","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376966893958856704"}
{"level":"INFO","ts":"2025-11-06 13:31:52.059","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376966897448517632"}
{"level":"INFO","ts":"2025-11-06 13:38:42.256","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0008604,"trackId":"376968617939435520"}
{"level":"INFO","ts":"2025-11-06 13:49:29.911","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0006377,"trackId":"376971334405586944"}
{"level":"INFO","ts":"2025-11-06 14:03:51.212","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009761,"trackId":"376974946959622144"}
{"level":"INFO","ts":"2025-11-06 14:06:06.794","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376975515635941376"}
{"level":"INFO","ts":"2025-11-06 14:18:02.722","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376978518455615488"}
{"level":"INFO","ts":"2025-11-06 14:18:38.073","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0008201,"trackId":"376978666724261888"}
{"level":"INFO","ts":"2025-11-06 14:19:30.070","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0002887,"trackId":"376978884819681280"}
{"level":"INFO","ts":"2025-11-06 14:20:01.974","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0008341,"trackId":"376979018630561792"}
{"level":"INFO","ts":"2025-11-06 14:20:12.293","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979061915779072"}
{"level":"INFO","ts":"2025-11-06 14:22:02.141","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009975,"trackId":"376979522647490560"}
{"level":"INFO","ts":"2025-11-06 14:22:19.654","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009764,"trackId":"376979596102336512"}
{"level":"INFO","ts":"2025-11-06 14:22:58.685","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979759810215936"}
{"level":"INFO","ts":"2025-11-06 14:22:59.290","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979762351964160"}
{"level":"INFO","ts":"2025-11-06 14:22:59.524","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979763333431296"}
{"level":"INFO","ts":"2025-11-06 14:22:59.723","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979764168097792"}
{"level":"INFO","ts":"2025-11-06 14:22:59.923","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979765006958592"}
{"level":"INFO","ts":"2025-11-06 14:23:00.125","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0001014,"trackId":"376979765854208000"}
{"level":"INFO","ts":"2025-11-06 14:23:08.297","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979800130060288"}
{"level":"INFO","ts":"2025-11-06 14:23:09.077","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0010337,"trackId":"376979803397423104"}
{"level":"INFO","ts":"2025-11-06 14:23:12.741","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979818765352960"}
{"level":"INFO","ts":"2025-11-06 14:23:13.561","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979822208876544"}
{"level":"INFO","ts":"2025-11-06 14:23:14.337","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979825463656448"}
{"level":"INFO","ts":"2025-11-06 14:23:15.086","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979828605190144"}
{"level":"INFO","ts":"2025-11-06 14:23:15.826","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979831708975104"}
{"level":"INFO","ts":"2025-11-06 14:23:16.041","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979832610750464"}
{"level":"INFO","ts":"2025-11-06 14:23:16.207","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979833307004928"}
{"level":"INFO","ts":"2025-11-06 14:23:16.410","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979834158448640"}
{"level":"INFO","ts":"2025-11-06 14:23:16.624","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009752,"trackId":"376979835051835392"}
{"level":"INFO","ts":"2025-11-06 14:23:16.824","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979835894890496"}
{"level":"INFO","ts":"2025-11-06 14:23:17.023","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979836729556992"}
{"level":"INFO","ts":"2025-11-06 14:23:17.222","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0004907,"trackId":"376979837564223488"}
{"level":"INFO","ts":"2025-11-06 14:23:17.443","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0002058,"trackId":"376979838491164672"}
{"level":"INFO","ts":"2025-11-06 14:23:17.622","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009347,"trackId":"376979839241945088"}
{"level":"INFO","ts":"2025-11-06 14:23:17.841","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979840160497664"}
{"level":"INFO","ts":"2025-11-06 14:23:18.041","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0002851,"trackId":"376979840999358464"}
{"level":"INFO","ts":"2025-11-06 14:23:18.259","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979841913716736"}
{"level":"INFO","ts":"2025-11-06 14:23:18.457","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.000401,"trackId":"376979842739994624"}
{"level":"INFO","ts":"2025-11-06 14:23:18.673","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979843650158592"}
{"level":"INFO","ts":"2025-11-06 14:23:18.891","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979844564516864"}
{"level":"INFO","ts":"2025-11-06 14:23:19.110","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0006448,"trackId":"376979845478875136"}
{"level":"INFO","ts":"2025-11-06 14:23:19.309","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979846317735936"}
{"level":"INFO","ts":"2025-11-06 14:23:19.525","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979847223705600"}
{"level":"INFO","ts":"2025-11-06 14:23:19.740","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0007353,"trackId":"376979848121286656"}
{"level":"INFO","ts":"2025-11-06 14:23:19.939","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979848960147456"}
{"level":"INFO","ts":"2025-11-06 14:23:20.140","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.000381,"trackId":"376979849803202560"}
{"level":"INFO","ts":"2025-11-06 14:23:20.376","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0005992,"trackId":"376979850793058304"}
{"level":"INFO","ts":"2025-11-06 14:23:20.591","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979851694833664"}
{"level":"INFO","ts":"2025-11-06 14:23:20.773","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0005447,"trackId":"376979852454002688"}
{"level":"INFO","ts":"2025-11-06 14:23:21.190","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0003462,"trackId":"376979854207221760"}
{"level":"INFO","ts":"2025-11-06 14:25:18.653","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376980346882752512"}
{"level":"INFO","ts":"2025-11-06 14:25:20.423","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376980354306670592"}
{"level":"INFO","ts":"2025-11-06 14:25:33.356","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.000976,"trackId":"376980408547409920"}
{"level":"INFO","ts":"2025-11-06 14:25:48.802","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376980473336823808"}
{"level":"INFO","ts":"2025-11-06 14:25:49.654","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009762,"trackId":"376980476906176512"}
{"level":"INFO","ts":"2025-11-06 14:26:01.687","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376980527380430848"}
{"level":"INFO","ts":"2025-11-06 16:01:53.899","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009752,"trackId":"377004653902036992"}
Whitespace-only changes.