This commit is contained in:
oneao committed 2025-11-05 22:25:09 +08:00
1 parent c7a1f83c67
commit 6849579652
19 files changed
+600 -249

No files matched your search

+2
View File
@@ -10,12 +10,14 @@
"web": "expo start --web"
},
"dependencies": {
"@alova/adapter-axios": "^2.0.16",
"@gluestack-ui/core": "^3.0.12",
"@gluestack-ui/utils": "^3.0.11",
"@gluestack/ui-next-adapter": "^3.0.3",
"@legendapp/motion": "^2.4.0",
"@react-native-async-storage/async-storage": "2.2.0",
"alova": "^3.3.4",
"axios": "^1.13.2",
"expo": "~54.0.20",
"expo-constants": "~18.0.10",
"expo-font": "~14.0.9",
+178 -8
View File
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@alova/adapter-axios':
specifier: ^2.0.16
version: 2.0.16(alova@3.3.4)(axios@1.13.2)
'@gluestack-ui/core':
specifier: ^3.0.12
version: 3.0.12(@gluestack-ui/utils@3.0.11(react-dom@19.2.0(react@19.1.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(yaml@2.8.1)))(react-native-safe-area-context@5.6.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-svg@15.12.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -26,6 +29,9 @@ importers:
alova:
specifier: ^3.3.4
version: 3.3.4
axios:
specifier: ^1.13.2
version: 1.13.2
expo:
specifier: ~54.0.20
version: 54.0.21(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -111,6 +117,12 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@alova/adapter-axios@2.0.16':
resolution: {integrity: sha512-O9iskfllA8Yd5QV7w/c2aBduf1ocpD5goXp3aJbBXovZanKbsgMyvuFycXmTeqPyLdjURo5fYicEmWHwURpkbQ==}
peerDependencies:
alova: ^3.0.20
axios: '>=0.4.0'
'@alova/shared@1.3.1':
resolution: {integrity: sha512-ijSOaFLUFcVzMKSY3avoEE5C03/p9atjMDPBwvNkwnzaCrhv6/m4A121NdadF8YlHCRuifyYfz90IyEdMXTsJg==}
@@ -2215,10 +2227,16 @@ packages:
async-limiter@1.0.1:
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@0.19.2:
resolution: {integrity: sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==}
deprecated: Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
babel-jest@29.7.0:
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -2355,6 +2373,10 @@ packages:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -2469,6 +2491,10 @@ packages:
resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
engines: {node: '>=0.1.90'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
@@ -2616,6 +2642,10 @@ packages:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -2677,6 +2707,10 @@ packages:
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
engines: {node: '>=12'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -2719,6 +2753,22 @@ packages:
error-stack-parser@2.1.4:
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -3153,6 +3203,15 @@ packages:
flow-enums-runtime@0.0.6:
resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
follow-redirects@1.5.10:
resolution: {integrity: sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==}
engines: {node: '>=4.0'}
@@ -3164,6 +3223,10 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
form-data@4.0.4:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
format@0.2.2:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
@@ -3199,6 +3262,10 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
@@ -3207,6 +3274,10 @@ packages:
resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
engines: {node: '>=8.0.0'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
@@ -3252,6 +3323,10 @@ packages:
globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -3266,6 +3341,14 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -3587,56 +3670,48 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.27.0:
resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.27.0:
resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.27.0:
resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.27.0:
resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
@@ -3737,6 +3812,10 @@ packages:
marky@1.3.0:
resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mdast-util-find-and-replace@3.0.2:
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
@@ -4371,6 +4450,9 @@ packages:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -5319,6 +5401,12 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
'@alova/adapter-axios@2.0.16(alova@3.3.4)(axios@1.13.2)':
dependencies:
'@alova/shared': 1.3.1
alova: 3.3.4
axios: 1.13.2
'@alova/shared@1.3.1': {}
'@antfu/eslint-config@6.2.0(@vue/compiler-sfc@3.5.22)(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3)':
@@ -8285,12 +8373,22 @@ snapshots:
async-limiter@1.0.1: {}
asynckit@0.4.0: {}
axios@0.19.2:
dependencies:
follow-redirects: 1.5.10
transitivePeerDependencies:
- supports-color
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.4
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
babel-jest@29.7.0(@babel/core@7.28.5):
dependencies:
'@babel/core': 7.28.5
@@ -8484,6 +8582,11 @@ snapshots:
cac@6.7.14: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
callsites@3.1.0: {}
camelcase-css@2.0.1: {}
@@ -8597,6 +8700,10 @@ snapshots:
colors@1.4.0: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@12.1.0: {}
commander@2.20.3: {}
@@ -8725,6 +8832,8 @@ snapshots:
define-lazy-prop@2.0.0: {}
delayed-stream@1.0.0: {}
depd@2.0.0: {}
dequal@2.0.3: {}
@@ -8776,6 +8885,12 @@ snapshots:
dotenv@16.4.7: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
eastasianwidth@0.2.0: {}
ee-first@1.1.1: {}
@@ -8805,6 +8920,21 @@ snapshots:
dependencies:
stackframe: 1.3.4
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
escalade@3.2.0: {}
escape-html@1.0.3: {}
@@ -9356,6 +9486,8 @@ snapshots:
flow-enums-runtime@0.0.6: {}
follow-redirects@1.15.11: {}
follow-redirects@1.5.10:
dependencies:
debug: 3.1.0
@@ -9369,6 +9501,14 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data@4.0.4:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
format@0.2.2: {}
freeport-async@2.0.0: {}
@@ -9392,10 +9532,28 @@ snapshots:
get-caller-file@2.0.5: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-nonce@1.0.1: {}
get-package-type@0.1.0: {}
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
get-tsconfig@4.13.0:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -9442,6 +9600,8 @@ snapshots:
globrex@0.1.2: {}
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
graphemer@1.4.0: {}
@@ -9450,6 +9610,12 @@ snapshots:
has-flag@4.0.0: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -9891,6 +10057,8 @@ snapshots:
marky@1.3.0: {}
math-intrinsics@1.1.0: {}
mdast-util-find-and-replace@3.0.2:
dependencies:
'@types/mdast': 4.0.4
@@ -10898,6 +11066,8 @@ snapshots:
kleur: 3.0.3
sisteransi: 1.0.5
proxy-from-env@1.1.0: {}
punycode@2.3.1: {}
qrcode-terminal@0.11.0: {}
@@ -1,6 +1,7 @@
import { Lock, UserRound } from 'lucide-react-native'
import { useState } from 'react'
import { Image, Text, View } from 'react-native'
import { AForm, AFormItem, AInput } from '@/components/common/ao'
import { IconQq, IconWeixin, IconZhiwen } from '@/components/iconfont'
import { Button, ButtonText } from '@/components/ui/button'
import {
@@ -10,13 +11,9 @@ import {
CheckboxLabel,
} from '@/components/ui/checkbox'
import { Divider } from '@/components/ui/divider'
import {
FormControl,
FormControlLabel,
FormControlLabelText,
} from '@/components/ui/form-control'
import { CheckIcon, EyeIcon, EyeOffIcon } from '@/components/ui/icon'
import { Input, InputField, InputIcon, InputSlot } from '@/components/ui/input'
import { CheckIcon } from '@/components/ui/icon'
import alovaInstance from '@/request'
import { useToast } from '@/utils/toast'
@@ -30,13 +27,14 @@ interface LoginForm {
export default function AuthLogin() {
const toast = useToast()
const [showPassword, setShowPassword] = useState(false)
const [form, setForm] = useState<LoginForm>({
username: '',
password: '',
rememberMe: true,
})
const handleInputChange = (field: keyof LoginForm, value: string) => {
setForm(prev => ({ ...prev, [field]: value }))
}
const defaultLogin = () => {
alovaInstance.Post('/auth/login/default', {
title: 'foo',
@@ -56,39 +54,21 @@ export default function AuthLogin() {
className="my-12"
/>
<FormControl size="md" className="w-full mb-6">
<FormControlLabel>
<FormControlLabelText>账号</FormControlLabelText>
</FormControlLabel>
<Input size="md">
<InputSlot className="pl-3">
<InputIcon as={UserRound} />
</InputSlot>
<InputField
placeholder="请输入账号"
onChangeText={text => setForm(prev => ({ ...prev, username: text }))}
<AForm form={form}>
<AFormItem label="账号">
<AInput
value={form.username}
onChange={text => handleInputChange('username', text)}
/>
</Input>
</FormControl>
<FormControl size="md" className="w-full mb-6">
<FormControlLabel>
<FormControlLabelText>密码</FormControlLabelText>
</FormControlLabel>
<Input size="md">
<InputSlot className="pl-3">
<InputIcon as={Lock} />
</InputSlot>
<InputField
type={showPassword ? 'text' : 'password'}
placeholder="请输入密码"
onChangeText={text => setForm(prev => ({ ...prev, password: text }))}
</AFormItem>
<AFormItem label="密码">
<AInput
isPassword
value={form.password}
onChange={text => handleInputChange('password', text)}
/>
<InputSlot className="pr-3" onPress={() => setShowPassword(prev => !prev)}>
<InputIcon as={showPassword ? EyeIcon : EyeOffIcon} />
</InputSlot>
</Input>
</FormControl>
</AFormItem>
</AForm>
<View className="w-full flex-row justify-between items-center mb-6">
<Checkbox
@@ -9,7 +9,7 @@ export interface FormRef {
interface FormProps {
children: React.ReactNode
form: Record<string, any>
scrollRef: React.RefObject<ScrollView | null>
scrollRef?: React.RefObject<ScrollView | null>
}
interface FormContextType {
@@ -57,7 +57,7 @@ const Form = forwardRef<FormRef, FormProps>(({ children, form, scrollRef }, ref)
const scrollToField = (name: string) => {
const y = fieldPositions.current.get(name)
if (y !== undefined && scrollRef.current) {
if (y !== undefined && scrollRef?.current) {
scrollRef.current.scrollTo({ y, animated: true })
}
}
@@ -22,7 +22,7 @@ interface RuleType {
}
interface FormItemProps<T> {
name: keyof T
name?: keyof T
label?: string
rules?: RuleType[]
children: React.ReactNode
+11 -10
View File
@@ -1,21 +1,22 @@
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'
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({
// ...
})
const alovaInstance = createAlova({
baseURL: 'http://192.168.1.136:8085/app',
requestAdapter: adapterFetch(),
// ...
beforeRequest: onAuthRequired((method) => {
// ...原请求前拦截器
}),
responded: onResponseRefreshToken((response, method) => {
// ...原响应成功拦截器
return response.json()
}),
// baseURL: 'http://192.168.1.136:8085/app',
baseURL: 'http://192.168.5.6:8085/app',
requestAdapter: axiosRequestAdapter(),
responded(response) {
// response自动被推断为AxiosResponse类型
return response.data
},
})
export default alovaInstance
@@ -15,7 +15,8 @@ db:
jwt:
secret: 3Bde3BGEbYqtqyEUzW3ry8jKFcaPH17fRmTmqE7MDr05Lwj95uruRKrrkb44TJ4s
expiry: -1
accessExpiry: 30m
refreshExpiry: 720h
log:
dir: "logs" # 日志根目录
@@ -4,9 +4,9 @@ import (
"base-go-v2/internal/db"
response "base-go-v2/internal/reponse"
"base-go-v2/internal/router"
"base-go-v2/internal/utils/jwtx"
"base-go-v2/internal/utils/mapx"
"base-go-v2/internal/utils/uid"
"fmt"
)
func InitAuthRouter(r *router.Router) {
@@ -15,20 +15,71 @@ func InitAuthRouter(r *router.Router) {
group.POST("/register/default", registerDefault)
}
func RefreshToken(c *router.Context) error {
bodyData, err := c.GetBodyWithRequired("accessToken", "refreshToken")
//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 loginDefault(c *router.Context) error {
// 获取请求体并校验必填字段
bodyData, err := c.GetBodyWithRequired("account", "password")
if err != nil {
return err
}
accessToken := bodyData.GetString("accessToken")
refreshToken := bodyData.GetString("refreshToken")
}
// 默认登录
func loginDefault(c *router.Context) error {
fmt.Println("loginDefault 被调用")
account := bodyData.GetString("account")
password := bodyData.GetString("password")
return response.Success(c).Send()
// 查询用户
userQuery := mapx.New().SetKV(
"account", account,
"password", password,
)
user, err := db.FindOne("user_info", userQuery)
if err != nil {
return response.Success(c).Message("查询用户失败,请稍后重试").IsSuccess(false).Send()
}
if user == nil {
// 账号或密码错误
return response.Success(c).
IsSuccess(false).
Message("账号或密码错误").
Send()
}
userId := user.GetInt("id")
// 创建 Access Token 和 Refresh Token
accessToken, err := jwtx.CreateAccessToken(userId)
if err != nil {
return err
}
refreshToken, err := jwtx.CreateRefreshToken(userId)
if err != nil {
return err
}
// 返回数据
respData := mapx.New().
SetIntToString("userId", userId).
SetKV(
"username", user.GetString("username"),
"seqNo", user.GetInt("seq_no"),
"gender", user.GetInt("gender"),
"accessToken", accessToken,
"refreshToken", refreshToken,
)
return response.Success(c).
Data(respData).
Send()
}
func registerDefault(c *router.Context) error {
@@ -48,7 +99,7 @@ func registerDefault(c *router.Context) error {
}
if len(existingUsers) > 0 {
return response.Error(c).Message("该账号已被注册").Send()
return response.Error(c).IsSuccess(false).Message("该账号已被注册").Send()
}
// 构造新用户数据
@@ -64,7 +115,7 @@ func registerDefault(c *router.Context) error {
return err
}
if insertedRows != 1 {
return response.Error(c).Message("注册失败").Send()
return response.Success(c).IsSuccess(false).Message("注册失败").Send()
}
return response.Success(c).Message("注册成功").Send()
@@ -18,8 +18,9 @@ func init() {
Port: 8080,
},
JWT: types.JWTConfig{
Secret: "default_secret",
Expiry: time.Hour,
Secret: "default_secret",
RefreshExpiry: time.Hour,
AccessExpiry: time.Hour,
},
Db: types.DbConfig{
MaxOpenConns: 10,
@@ -1,17 +1,18 @@
package db
import (
"base-go-v2/internal/utils/mapx"
"fmt"
"strings"
)
// ---------------- 通用查询方法 ----------------
// GetOne 根据主键查询单条记录,返回 map[string]interface{}
func GetOne(table string, pkColumn string, pkValue interface{}) (map[string]interface{}, error) {
// GetOne 根据主键查询单条记录,返回 mapx.M
func GetOne(table string, pkColumn string, pkValue interface{}) (mapx.M, error) {
query := fmt.Sprintf("SELECT * FROM %s WHERE %s=$1", table, pkColumn)
row := make(map[string]interface{})
row := mapx.New()
err := DB.Get(&row, query, pkValue)
if err != nil {
return nil, err
@@ -19,13 +20,13 @@ func GetOne(table string, pkColumn string, pkValue interface{}) (map[string]inte
return row, nil
}
// GetBatch 根据主键批量查询,返回 []map[string]interface{}
func GetBatch(table string, pkColumn string, pkValues []interface{}) ([]map[string]interface{}, error) {
// GetBatch 根据主键批量查询,返回 []mapx.M
func GetBatch(table string, pkColumn string, pkValues []interface{}) ([]mapx.M, error) {
if len(pkValues) == 0 {
return nil, nil
}
var rows []map[string]interface{}
var rows []mapx.M
// 构建 IN 占位符
placeholders := make([]string, len(pkValues))
@@ -42,10 +43,8 @@ func GetBatch(table string, pkColumn string, pkValues []interface{}) ([]map[stri
return rows, nil
}
// Find 根据条件查询多条记录,可选排序
// conditions: map[column]value, 将以 AND 拼接
// orderBy: 可选排序字段,如 "created_at ASC",为空默认 "b_id DESC"
func Find(table string, conditions map[string]interface{}, orderBy ...string) ([]map[string]interface{}, error) {
// Find 根据条件查询多条记录,可选排序,返回 []mapx.M
func Find(table string, conditions map[string]interface{}, orderBy ...string) ([]mapx.M, error) {
if len(conditions) == 0 {
return nil, fmt.Errorf("查询条件不能为空")
}
@@ -67,7 +66,7 @@ func Find(table string, conditions map[string]interface{}, orderBy ...string) ([
}
query += " ORDER BY " + order
var rows []map[string]interface{}
var rows []mapx.M
err := DB.Select(&rows, query, values...)
if err != nil {
return nil, err
@@ -76,8 +75,8 @@ func Find(table string, conditions map[string]interface{}, orderBy ...string) ([
return rows, nil
}
// FindAll 查询整个表,可选排序
func FindAll(table string, orderBy ...string) ([]map[string]interface{}, error) {
// FindAll 查询整个表,可选排序,返回 []mapx.M
func FindAll(table string, orderBy ...string) ([]mapx.M, error) {
query := fmt.Sprintf("SELECT * FROM %s", table)
order := "b_id DESC"
@@ -86,7 +85,7 @@ func FindAll(table string, orderBy ...string) ([]map[string]interface{}, error)
}
query += " ORDER BY " + order
var rows []map[string]interface{}
var rows []mapx.M
err := DB.Select(&rows, query)
if err != nil {
return nil, err
@@ -94,3 +93,35 @@ func FindAll(table string, orderBy ...string) ([]map[string]interface{}, error)
return rows, nil
}
// FindOne 根据条件查询单条记录,返回 mapx.M
func FindOne(table string, conditions map[string]interface{}, orderBy ...string) (mapx.M, error) {
if len(conditions) == 0 {
return nil, fmt.Errorf("查询条件不能为空")
}
var whereParts []string
var values []interface{}
i := 1
for k, v := range conditions {
whereParts = append(whereParts, fmt.Sprintf("%s=$%d", k, i))
values = append(values, v)
i++
}
query := fmt.Sprintf("SELECT * FROM %s WHERE %s", table, strings.Join(whereParts, " AND "))
order := "b_id DESC"
if len(orderBy) > 0 && orderBy[0] != "" {
order = orderBy[0]
}
query += " ORDER BY " + order + " LIMIT 1" // 只取一条记录
row := mapx.New()
err := DB.Get(&row, query, values...)
if err != nil {
return nil, err
}
return row, nil
}
@@ -1,19 +1,34 @@
package errs
import "fmt"
import (
"fmt"
"strings"
)
// ValidationError 用于收集多个字段错误,同时实现 error 接口
type ValidationError struct {
field string
message string
Errors map[string]string // key: 字段名, value: 错误信息
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("字段 %s: %s", e.field, e.message)
// Error 实现 error 接口
func (v *ValidationError) Error() string {
if len(v.Errors) == 0 {
return ""
}
msgs := make([]string, 0, len(v.Errors))
for field, msg := range v.Errors {
msgs = append(msgs, fmt.Sprintf("%s: %s", field, msg))
}
return strings.Join(msgs, "; ")
}
func NewValidationError(field, message string) error {
// NewValidationError 创建多字段的 ValidationError
func NewValidationError(errors map[string]string) error {
if len(errors) == 0 {
return nil
}
return &ValidationError{
field: field,
message: message,
Errors: errors,
}
}
@@ -8,18 +8,22 @@ import (
"net/http"
)
// Result 是最终响应结构体
type Result struct {
Code string `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
TrackId string `json:"trackId"`
IsSuccess bool `json:"isSuccess"` // 是否成功
Code string `json:"code"` // 状态码
Message string `json:"message"` // 提示信息
Data interface{} `json:"data,omitempty"` // 数据
TrackId string `json:"trackId,omitempty"` // 请求追踪ID
}
// Code 定义状态码
type Code struct {
Value string
Message string
}
// 常用状态码
var (
CodeSuccess = Code{"0000", "请求成功"}
CodeFail = Code{"0001", "请求失败"}
@@ -31,53 +35,75 @@ var (
const defaultMessage = "未知错误"
// Builder 用于构建响应
type Builder struct {
c *router.Context
result Result
}
// ---------------- 构造函数 ----------------
// Success 创建成功响应
func Success(c *router.Context) *Builder {
return &Builder{
c: c,
result: Result{
Code: CodeSuccess.Value,
Message: CodeSuccess.Message,
IsSuccess: true,
Code: CodeSuccess.Value,
Message: CodeSuccess.Message,
},
}
}
// Error 创建失败响应
func Error(c *router.Context) *Builder {
return &Builder{
c: c,
result: Result{
Code: CodeFail.Value,
Message: CodeFail.Message,
IsSuccess: false,
Code: CodeFail.Value,
Message: CodeFail.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 {
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 {
@@ -90,10 +116,12 @@ func (b *Builder) Send() error {
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,
@@ -97,7 +97,7 @@ func (c *Context) GetBodyWithRequired(keys ...string) (mapx.M, error) {
}
// 校验必填字段
if err := validate.ValidateNotEmpty(req, keys...); err != nil {
if err := validate.NotEmpty(req, keys...); err != nil {
return nil, err
}
@@ -17,8 +17,9 @@ type ServerConfig struct {
}
type JWTConfig struct {
Secret string
Expiry time.Duration
Secret string
AccessExpiry time.Duration
RefreshExpiry time.Duration
}
type DbConfig struct {
@@ -1,77 +0,0 @@
package jwt
import (
"base-go-v2/internal/config"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrTokenExpired = errors.New("token expired")
ErrTokenInvalid = errors.New("token invalid")
)
// CustomClaims 定义自己的 payload 结构
type CustomClaims struct {
UserID string `json:"user_id"`
jwt.RegisteredClaims
}
// CreateToken 创建一个 JWT token
// 支持 config.App.JWT.Expiry == -1 永不过期
func CreateToken(orgID, userID string) (string, error) {
claims := CustomClaims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
expiry := config.App.JWT.Expiry
switch {
case expiry > 0:
expireAt := time.Now().Add(expiry)
claims.ExpiresAt = jwt.NewNumericDate(expireAt)
case expiry == -1:
// 永不过期,ExpiresAt 设置为 nil
claims.ExpiresAt = nil
default:
// 默认过期时间,例如 1 小时
expireAt := time.Now().Add(time.Hour)
claims.ExpiresAt = jwt.NewNumericDate(expireAt)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.App.JWT.Secret))
}
// VerifyToken 验证并解析 JWT token
func VerifyToken(tokenString string) (*CustomClaims, error) {
signKey := []byte(config.App.JWT.Secret)
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
// 校验签名算法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, ErrTokenInvalid
}
return signKey, nil
})
if err != nil {
return nil, ErrTokenInvalid
}
claims, ok := token.Claims.(*CustomClaims)
if !ok || !token.Valid {
return nil, ErrTokenInvalid
}
// 检查过期:ExpiresAt 为 nil 时永不过期
if claims.ExpiresAt != nil && time.Now().After(claims.ExpiresAt.Time) {
return nil, ErrTokenExpired
}
return claims, nil
}
@@ -0,0 +1,82 @@
package jwtx
import (
"base-go-v2/internal/config"
"time"
"github.com/golang-jwt/jwt/v5"
)
// CustomClaims 自定义 payload,可扩展其他信息
type CustomClaims struct {
UserID int `json:"user_id"` // 改为 int
jwt.RegisteredClaims
}
// TokenResult 用于返回验证结果
type TokenResult struct {
Claims *CustomClaims
Valid bool
Expired bool
}
// CreateAccessToken 创建 Access Token,只返回 token 和 error
func CreateAccessToken(userID int) (string, error) {
return createToken(userID, config.App.JWT.AccessExpiry)
}
// CreateRefreshToken 创建 Refresh Token,只返回 token 和 error
func CreateRefreshToken(userID int) (string, error) {
return createToken(userID, config.App.JWT.RefreshExpiry)
}
// 内部方法,根据过期时间生成 token
func createToken(userID int, expiry time.Duration) (string, error) {
claims := &CustomClaims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
switch {
case expiry > 0:
expireAt := time.Now().Add(expiry)
claims.ExpiresAt = jwt.NewNumericDate(expireAt)
case expiry == -1:
claims.ExpiresAt = nil // 永不过期
default:
expireAt := time.Now().Add(time.Hour)
claims.ExpiresAt = jwt.NewNumericDate(expireAt)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(config.App.JWT.Secret))
if err != nil {
return "", err
}
return signed, nil
}
// VerifyToken 验证 token 并返回结构化结果
func VerifyToken(tokenString string) TokenResult {
result := TokenResult{Valid: false}
token, _ := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(config.App.JWT.Secret), nil
})
claims, ok := token.Claims.(*CustomClaims)
if !ok || !token.Valid {
return result // token 无效
}
result.Claims = claims
result.Valid = true
if claims.ExpiresAt != nil && time.Now().After(claims.ExpiresAt.Time) {
result.Expired = true
}
return result
}
@@ -1,5 +1,10 @@
package mapx
import (
"encoding/json"
"strconv"
)
// M 是 map[string]interface{} 的别名
type M map[string]interface{}
@@ -29,6 +34,35 @@ func (m M) SetKV(kvs ...interface{}) M {
return m
}
// ---------------- 新增方法 ----------------
// SetIntToString 将 int 或 int64 直接转为 string 存储,用于前端安全传输
func (m M) SetIntToString(key string, val interface{}) M {
var s string
switch v := val.(type) {
case int:
s = strconv.Itoa(v) // 直接转为 string
case int64:
s = strconv.FormatInt(v, 10)
default:
panic("SetIntToString: unsupported type")
}
m[key] = s
return m
}
// GetIntFromString 将前端传来的字符串解析为 int64
func (m M) GetIntFromString(key string) int64 {
if v, ok := m[key]; ok {
if s, ok := v.(string); ok {
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
return i
}
}
}
return 0
}
// Get 获取值
func (m M) Get(key string) interface{} {
return m[key]
@@ -62,44 +96,15 @@ func (m M) GetInt(key string) int {
return int(val)
case float64:
return int(val)
case string:
if i, err := strconv.ParseInt(val, 10, 64); err == nil {
return int(i)
}
}
}
return 0
}
// GetFloat 获取浮点数类型
func (m M) GetFloat(key string) float64 {
if v, ok := m[key]; ok {
switch val := v.(type) {
case float32:
return float64(val)
case float64:
return val
case int:
return float64(val)
case int8:
return float64(val)
case int16:
return float64(val)
case int32:
return float64(val)
case int64:
return float64(val)
}
}
return 0
}
// GetBool 获取布尔类型
func (m M) GetBool(key string) bool {
if v, ok := m[key]; ok {
if b, ok := v.(bool); ok {
return b
}
}
return false
}
// Merge 合并另一个 M,后者会覆盖前者同名字段
func (m M) Merge(other M) M {
for k, v := range other {
@@ -107,3 +112,8 @@ func (m M) Merge(other M) M {
}
return m
}
// MarshalJSON 支持 M 自定义 JSON 序列化
func (m M) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}(m))
}
@@ -1,42 +1,65 @@
package validate
import (
"base-go-v2/internal/errs"
"errors"
"reflect"
"fmt"
"strings"
)
// ValidateNotEmpty 校验结构体中指定字段不能为空
func ValidateNotEmpty(s interface{}, fields ...string) error {
v := reflect.ValueOf(s)
if v.Kind() == reflect.Ptr {
v = v.Elem()
// ValidationError 用于收集多个字段错误,同时实现 error 接口
type ValidationError struct {
Errors map[string]string
}
// Error 实现 error 接口
func (v *ValidationError) Error() string {
msgs := make([]string, 0, len(v.Errors))
for field, err := range v.Errors {
msgs = append(msgs, fmt.Sprintf("%s: %s", field, err))
}
return strings.Join(msgs, "; ")
}
// NotEmpty 校验 map[string]interface{} 中指定字段不能为空
func NotEmpty(data map[string]interface{}, fields ...string) error {
errs := make(map[string]string)
for _, field := range fields {
f := v.FieldByName(field)
if !f.IsValid() {
return errors.New("field " + field + " does not exist")
}
switch f.Kind() {
case reflect.String:
if strings.TrimSpace(f.String()) == "" {
return errs.NewValidationError(field, "不能为空")
}
case reflect.Slice, reflect.Array, reflect.Map:
if f.Len() == 0 {
return errs.NewValidationError(field, "不能为空")
}
case reflect.Ptr, reflect.Interface:
if f.IsNil() {
return errs.NewValidationError(field, "不能为空")
}
default:
continue
}
collectErrors(data[field], field, errs)
}
if len(errs) > 0 {
return &ValidationError{Errors: errs}
}
return nil
}
// collectErrors 递归收集字段错误
func collectErrors(v interface{}, path string, errs map[string]string) {
if v == nil {
errs[path] = fmt.Sprintf("字段 %s 不能为空", path)
return
}
switch val := v.(type) {
case string:
if strings.TrimSpace(val) == "" {
errs[path] = fmt.Sprintf("字段 %s 不能为空", path)
}
case []interface{}:
if len(val) == 0 {
errs[path] = fmt.Sprintf("字段 %s 不能为空", 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)
}
for k, elem := range val {
collectErrors(elem, fmt.Sprintf("%s.%s", path, k), errs)
}
default:
// 其他类型暂不校验
}
}
@@ -1,3 +1,35 @@
{"level":"INFO","ts":"2025-11-05 16:59:43.475","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0009773,"trackId":"376656818492932096"}
{"level":"INFO","ts":"2025-11-05 17:00:17.955","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0009184,"trackId":"376656963112534016"}
{"level":"INFO","ts":"2025-11-05 17:00:21.027","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376656976001630208"}
{"level":"INFO","ts":"2025-11-05 21:35:40.062","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0005206,"trackId":"376726261852012544"}
{"level":"INFO","ts":"2025-11-05 21:35:47.481","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0008274,"trackId":"376726292969553920"}
{"level":"INFO","ts":"2025-11-05 21:35:48.665","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726297939804160"}
{"level":"INFO","ts":"2025-11-05 21:35:48.907","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726298954825728"}
{"level":"INFO","ts":"2025-11-05 21:35:49.171","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0009767,"trackId":"376726300057927680"}
{"level":"INFO","ts":"2025-11-05 21:37:38.008","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0005298,"trackId":"376726756557586432"}
{"level":"INFO","ts":"2025-11-05 21:38:18.445","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726926162657280"}
{"level":"INFO","ts":"2025-11-05 21:38:20.520","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0004533,"trackId":"376726934865838080"}
{"level":"INFO","ts":"2025-11-05 21:38:21.140","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726937466306560"}
{"level":"INFO","ts":"2025-11-05 21:38:21.288","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0004953,"trackId":"376726938082869248"}
{"level":"INFO","ts":"2025-11-05 21:38:21.609","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726939433435136"}
{"level":"INFO","ts":"2025-11-05 21:38:21.828","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726940351987712"}
{"level":"INFO","ts":"2025-11-05 21:38:22.088","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.000925,"trackId":"376726941438312448"}
{"level":"INFO","ts":"2025-11-05 21:38:22.281","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0002126,"trackId":"376726942252007424"}
{"level":"INFO","ts":"2025-11-05 21:38:22.476","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0002409,"trackId":"376726943069896704"}
{"level":"INFO","ts":"2025-11-05 21:38:22.663","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726943854231552"}
{"level":"INFO","ts":"2025-11-05 21:38:22.883","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726944776978432"}
{"level":"INFO","ts":"2025-11-05 21:38:23.100","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0007084,"trackId":"376726945682948096"}
{"level":"INFO","ts":"2025-11-05 21:38:23.324","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726946626666496"}
{"level":"INFO","ts":"2025-11-05 21:38:23.548","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726947566190592"}
{"level":"INFO","ts":"2025-11-05 21:38:23.796","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0007185,"trackId":"376726948606377984"}
{"level":"INFO","ts":"2025-11-05 21:38:23.992","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0002713,"trackId":"376726949424267264"}
{"level":"INFO","ts":"2025-11-05 21:38:24.230","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0006165,"trackId":"376726950422511616"}
{"level":"INFO","ts":"2025-11-05 21:38:24.432","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726951273955328"}
{"level":"INFO","ts":"2025-11-05 21:38:24.628","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726952096038912"}
{"level":"INFO","ts":"2025-11-05 21:38:25.076","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726953975087104"}
{"level":"INFO","ts":"2025-11-05 21:38:25.276","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0000358,"trackId":"376726954813947904"}
{"level":"INFO","ts":"2025-11-05 21:38:25.459","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726955581505536"}
{"level":"INFO","ts":"2025-11-05 21:38:25.664","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0004416,"trackId":"376726956437143552"}
{"level":"INFO","ts":"2025-11-05 21:38:25.864","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0005154,"trackId":"376726957276004352"}
{"level":"INFO","ts":"2025-11-05 21:38:26.084","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0.0002596,"trackId":"376726958202945536"}
{"level":"INFO","ts":"2025-11-05 21:38:26.319","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.5.3","elapsed":0,"trackId":"376726959188606976"}