diff --git a/code/allapp/allapp-rn/pnpm-lock.yaml b/code/allapp/allapp-rn/pnpm-lock.yaml index 541342de..ef9f968a 100644 --- a/code/allapp/allapp-rn/pnpm-lock.yaml +++ b/code/allapp/allapp-rn/pnpm-lock.yaml @@ -4144,48 +4144,56 @@ 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==} @@ -6279,7 +6287,7 @@ snapshots: '@babel/generator@7.28.5': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.28.6 '@babel/types': 7.28.6 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -6860,8 +6868,8 @@ snapshots: '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.5 + '@babel/code-frame': 7.28.6 + '@babel/parser': 7.28.6 '@babel/types': 7.28.6 '@babel/template@7.28.6': diff --git a/code/app/app-go/.gitignore b/code/app/app-go/.gitignore new file mode 100644 index 00000000..101ea87c --- /dev/null +++ b/code/app/app-go/.gitignore @@ -0,0 +1,37 @@ +*.o +*.a +*.so +_obj +_test +*.[568vq] +[568vq].out +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* +_testmain.go +*.exe +*.exe~ +*.test +*.prof +*.rar +*.zip +*.gz +*.psd +*.bmd +*.cfg +*.pptx +*.log +*nohup.out +*settings.pyc +*.sublime-project +*.sublime-workspace +!.gitkeep +.DS_Store +/.idea +/.vscode +/output +*.local.yml +dumped_hertz_remote_config.json + \ No newline at end of file diff --git a/code/app/app-go/cmd/app/main.go b/code/app/app-go/cmd/app/main.go new file mode 100644 index 00000000..eaf62798 --- /dev/null +++ b/code/app/app-go/cmd/app/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "allapp-go/internal/bootstrap" +) + +func main() { + app, err := bootstrap.NewApp() + if err != nil { + panic(err) + } + + defer app.Close() + + if err := bootstrap.RunServer(app.Config); err != nil { + panic(err) + } +} diff --git a/code/app/app-go/configs/config.dev.yaml b/code/app/app-go/configs/config.dev.yaml new file mode 100644 index 00000000..5a3b584a --- /dev/null +++ b/code/app/app-go/configs/config.dev.yaml @@ -0,0 +1,13 @@ +app: + port: 8087 +postgres: + host: 117.72.182.135 + port: 5432 + user: postgres + password: zhang520.. + dbname: app + timezone: Asia/Shanghai + max_open_conns: 10 + max_idle_conns: 5 + conn_max_lifetime: 30m + sll_mode: false \ No newline at end of file diff --git a/code/app/app-go/configs/config.prod.yaml b/code/app/app-go/configs/config.prod.yaml new file mode 100644 index 00000000..e69de29b diff --git a/code/app/app-go/configs/config.yaml b/code/app/app-go/configs/config.yaml new file mode 100644 index 00000000..9be48ad0 --- /dev/null +++ b/code/app/app-go/configs/config.yaml @@ -0,0 +1,3 @@ +app: + name: allapp + port: 8080 diff --git a/code/app/app-go/go.mod b/code/app/app-go/go.mod new file mode 100644 index 00000000..430219b0 --- /dev/null +++ b/code/app/app-go/go.mod @@ -0,0 +1,51 @@ +module allapp-go + +go 1.25.0 + +require ( + github.com/go-playground/validator/v10 v10.30.2 + github.com/gofiber/fiber/v3 v3.1.0 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/jackc/pgx/v5 v5.9.1 + github.com/spf13/viper v1.21.0 + go.uber.org/zap v1.27.1 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 +) + +require ( + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gofiber/schema v1.7.0 // indirect + github.com/gofiber/utils/v2 v2.0.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.18.4 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tinylib/msgp v1.6.3 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.69.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect +) diff --git a/code/app/app-go/go.sum b/code/app/app-go/go.sum new file mode 100644 index 00000000..f98ce49b --- /dev/null +++ b/code/app/app-go/go.sum @@ -0,0 +1,123 @@ +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= +github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= +github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg= +github.com/gofiber/schema v1.7.0/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= +github.com/gofiber/utils/v2 v2.0.2 h1:ShRRssz0F3AhTlAQcuEj54OEDtWF7+HJDwEi/aa6QLI= +github.com/gofiber/utils/v2 v2.0.2/go.mod h1:+9Ub4NqQ+IaJoTliq5LfdmOJAA/Hzwf4pXOxOa3RrJ0= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= +github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= +github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= +github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= +github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/code/app/app-go/internal/bootstrap/app.go b/code/app/app-go/internal/bootstrap/app.go new file mode 100644 index 00000000..10884628 --- /dev/null +++ b/code/app/app-go/internal/bootstrap/app.go @@ -0,0 +1,58 @@ +package bootstrap + +import ( + "allapp-go/internal/bootstrap/boots" + "allapp-go/internal/config" + "allapp-go/pkg/db" + "allapp-go/pkg/logger" + + "github.com/jackc/pgx/v5/pgxpool" +) + +type App struct { + Config *config.Config + PgSql *pgxpool.Pool +} + +// ====================== +// 初始化 +// ====================== + +func NewApp() (*App, error) { + cfg, err := config.Load() + if err != nil { + return nil, err + } + + // 日志 + boots.InitLogger(cfg) + + // 数据库 + pgsql, err := boots.InitPgsql(cfg) + if err != nil { + return nil, err + } + + db.Init(pgsql) + + // 其他组件 + boots.InitUniqueId(cfg) + boots.InitJwt(cfg) + + return &App{ + Config: cfg, + PgSql: pgsql, + }, nil +} + +// ====================== +// 释放资源 +// ====================== + +func (a *App) Close() { + if a.PgSql != nil { + a.PgSql.Close() + } + + logger.Sync() +} diff --git a/code/app/app-go/internal/bootstrap/boots/config.go b/code/app/app-go/internal/bootstrap/boots/config.go new file mode 100644 index 00000000..b30d446c --- /dev/null +++ b/code/app/app-go/internal/bootstrap/boots/config.go @@ -0,0 +1,18 @@ +package boots + +import ( + "allapp-go/internal/config" + "allapp-go/pkg/logger" +) + +func InitLogger(cfg *config.Config) { + logger.Init(logger.Config{ + Dir: "logs", + Level: "info", + MaxSize: 100, // MB + MaxBackups: 10, + MaxAge: 7, // 天 + Compress: true, + }) + defer logger.Sync() +} diff --git a/code/app/app-go/internal/bootstrap/boots/jwt.go b/code/app/app-go/internal/bootstrap/boots/jwt.go new file mode 100644 index 00000000..4a231c4b --- /dev/null +++ b/code/app/app-go/internal/bootstrap/boots/jwt.go @@ -0,0 +1,11 @@ +package boots + +import ( + "allapp-go/internal/config" + "allapp-go/pkg/jwtx" +) + +// InitJwt 初始化Jwt +func InitJwt(cfg *config.Config) { + jwtx.Init(cfg.JWT.Secret, cfg.JWT.AccessExpiry) +} diff --git a/code/app/app-go/internal/bootstrap/boots/pgsql.go b/code/app/app-go/internal/bootstrap/boots/pgsql.go new file mode 100644 index 00000000..3b6a11ce --- /dev/null +++ b/code/app/app-go/internal/bootstrap/boots/pgsql.go @@ -0,0 +1,84 @@ +package boots + +import ( + "allapp-go/internal/config" + "context" + "fmt" + "net/url" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func InitPgsql(cfg *config.Config) (*pgxpool.Pool, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + pg := cfg.Postgres + + // ssl + sslMode := "disable" + if pg.SslMode { + sslMode = "require" + } + + // 密码转义 + password := url.QueryEscape(pg.Password) + + // DSN + dsn := fmt.Sprintf( + "postgres://%s:%s@%s:%d/%s?sslmode=%s", + pg.User, + password, + pg.Host, + pg.Port, + pg.Dbname, + sslMode, + ) + + // 解析配置 + conf, err := pgxpool.ParseConfig(dsn) + if err != nil { + return nil, err + } + + // ====================== + // 连接池配置 + // ====================== + + // 最大连接数 + if pg.MaxOpenConns > 0 { + conf.MaxConns = int32(pg.MaxOpenConns) + } + + // 最小连接数(近似 idle) + if pg.MaxIdleConns > 0 { + conf.MinConns = int32(pg.MaxIdleConns) + } + + // 连接最大生命周期 + if pg.ConnMaxLifetime > 0 { + conf.MaxConnLifetime = pg.ConnMaxLifetime + } + + // ====================== + // 时区设置(正确方式 ⭐) + // ====================== + if pg.TimeZone != "" { + conf.ConnConfig.RuntimeParams["TimeZone"] = pg.TimeZone + } + + // 创建连接池 + db, err := pgxpool.NewWithConfig(ctx, conf) + if err != nil { + return nil, err + } + + // 测试连接 + if err := db.Ping(ctx); err != nil { + db.Close() + return nil, err + } + + return db, nil +} diff --git a/code/app/app-go/internal/bootstrap/boots/uniqueid.go b/code/app/app-go/internal/bootstrap/boots/uniqueid.go new file mode 100644 index 00000000..336d8831 --- /dev/null +++ b/code/app/app-go/internal/bootstrap/boots/uniqueid.go @@ -0,0 +1,12 @@ +package boots + +import ( + "allapp-go/internal/config" + "allapp-go/pkg/uniqueid" +) + +// InitUniqueId 初始化雪花id +func InitUniqueId(cfg *config.Config) { + options := uniqueid.NewIdGeneratorOptions(cfg.UniqueID.WorkerID) + uniqueid.SetIdGenerator(options) +} diff --git a/code/app/app-go/internal/bootstrap/server.go b/code/app/app-go/internal/bootstrap/server.go new file mode 100644 index 00000000..a6bc7061 --- /dev/null +++ b/code/app/app-go/internal/bootstrap/server.go @@ -0,0 +1,34 @@ +package bootstrap + +import ( + "allapp-go/internal/config" + "allapp-go/internal/errors" + "allapp-go/internal/middleware" + "allapp-go/internal/router" + "allapp-go/pkg/logger" + "fmt" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/recover" + "github.com/gofiber/fiber/v3/middleware/requestid" +) + +func RunServer(cfg *config.Config) error { + app := fiber.New(fiber.Config{ + ErrorHandler: errors.ErrorHandler(), + }) + + // ===== middleware ===== + app.Use(recover.New()) + app.Use(requestid.New()) + app.Use(middleware.Logger()) + + // ===== router ===== + router.SetupRouter(app) + + addr := fmt.Sprintf(":%d", cfg.App.Port) + + logger.Log.Info("服务启动") + + return app.Listen(addr) +} diff --git a/code/app/app-go/internal/config/config.go b/code/app/app-go/internal/config/config.go new file mode 100644 index 00000000..9f1adb99 --- /dev/null +++ b/code/app/app-go/internal/config/config.go @@ -0,0 +1,38 @@ +package config + +import "time" + +type Config struct { + App AppConfig `mapstructure:"app"` + JWT JwtConfig `mapstructure:"jwt"` + UniqueID UniqueIDConfig `mapstructure:"unique_id"` + Postgres PostgresConfig `mapstructure:"postgres"` +} + +type AppConfig struct { + Name string `mapstructure:"name"` + Port int `mapstructure:"port"` +} + +type PostgresConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + Dbname string `mapstructure:"dbname"` + SslMode bool `mapstructure:"ssl_mode"` + TimeZone string `mapstructure:"timezone"` + MaxOpenConns int32 `mapstructure:"max_open_conns"` + MaxIdleConns int32 `mapstructure:"max_idle_conns"` + ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` +} + +type JwtConfig struct { + Secret string `mapstructure:"secret"` + AccessExpiry time.Duration `mapstructure:"access_expiry"` +} + +type UniqueIDConfig struct { + DataCenterID uint16 `mapstructure:"datacenter_id"` + WorkerID uint16 `mapstructure:"worker_id"` +} diff --git a/code/app/app-go/internal/config/loader.go b/code/app/app-go/internal/config/loader.go new file mode 100644 index 00000000..c1bb3d04 --- /dev/null +++ b/code/app/app-go/internal/config/loader.go @@ -0,0 +1,67 @@ +package config + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/viper" +) + +func Load() (*Config, error) { + env := getEnv("APP_ENV", "dev") + + v := viper.New() + v.SetConfigType("yaml") + + // ✅ 默认值 + setDefaults(v) + + // ✅ 支持环境变量(如 APP_PORT → app.port) + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // 基础配置 + v.AddConfigPath("configs") + v.SetConfigName("config") + + if err := v.ReadInConfig(); err != nil { + return nil, fmt.Errorf("读取基础配置失败: %w", err) + } + + // 环境配置(覆盖) + envConfigName := fmt.Sprintf("config.%s", env) + v.SetConfigName(envConfigName) + + if err := v.MergeInConfig(); err != nil { + // dev 可以没有,prod 必须有 + if env == "prod" { + return nil, fmt.Errorf("读取环境配置失败(%s): %w", env, err) + } + } + + var cfg Config + if err := v.Unmarshal(&cfg); err != nil { + return nil, fmt.Errorf("解析配置失败: %w", err) + } + + return &cfg, nil +} + +// ✅ 默认值统一在这里管理 +func setDefaults(v *viper.Viper) { + v.SetDefault("app.name", "allapp-go") + v.SetDefault("app.port", 8080) + + // 可以继续扩展 + v.SetDefault("unique_id.datacenter_id", 1) + v.SetDefault("unique_id.worker_id", 1) +} + +func getEnv(key, defaultVal string) string { + val := os.Getenv(key) + if val == "" { + return defaultVal + } + return strings.ToLower(val) +} diff --git a/code/app/app-go/internal/errors/errors.go b/code/app/app-go/internal/errors/errors.go new file mode 100644 index 00000000..5d4be75d --- /dev/null +++ b/code/app/app-go/internal/errors/errors.go @@ -0,0 +1,10 @@ +package errors + +import "github.com/pkg/errors" + +func WithStack(err error) error { + if err == nil { + return nil + } + return errors.WithStack(err) +} diff --git a/code/app/app-go/internal/errors/handle.go b/code/app/app-go/internal/errors/handle.go new file mode 100644 index 00000000..1221eb54 --- /dev/null +++ b/code/app/app-go/internal/errors/handle.go @@ -0,0 +1,31 @@ +package errors + +import ( + "allapp-go/internal/httpx" + "allapp-go/pkg/logger" + "errors" + "fmt" + + "github.com/gofiber/fiber/v3" + "go.uber.org/zap" +) + +func ErrorHandler() fiber.ErrorHandler { + return func(c fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + msg := "系统异常,请稍后再试!" + + var e *fiber.Error + if errors.As(err, &e) { + code = e.Code + msg = e.Message + } + + logger.FromCtx(c.Context()).Error("HTTP 请求异常", + zap.Int("status", code), + zap.String("error", fmt.Sprintf("%+v", err)), + ) + + return httpx.SystemError(c, msg) + } +} diff --git a/code/app/app-go/internal/handle/data_list.go b/code/app/app-go/internal/handle/data_list.go new file mode 100644 index 00000000..ded853a1 --- /dev/null +++ b/code/app/app-go/internal/handle/data_list.go @@ -0,0 +1,50 @@ +package handle + +import ( + "allapp-go/internal/errors" + "allapp-go/internal/httpx" + "allapp-go/internal/types" + "allapp-go/pkg/db" + + "github.com/gofiber/fiber/v3" +) + +func LoadData(c fiber.Ctx) error { + var req types.LoadDataReq + + err := httpx.BindAndValidate(c, &req) + + if err != nil { + return errors.WithStack(err) + } + + dbClient := db.New() + // 查询 + data, err := dbClient.LoadData(c.Context(), req.ViewName, req.SearchCondition, req.OrderBy, req.SearchColumns, req.Args) + + if err != nil { + return errors.WithStack(err) + } + + return httpx.OK(c, data) +} + +func LoadDataBySql(c fiber.Ctx) error { + var req types.LoadDataBySqlReq + + err := httpx.BindAndValidate(c, &req) + + if err != nil { + return errors.WithStack(err) + } + + dbClient := db.New() + + data, err := dbClient.LoadDataBySQL(c.Context(), req.Sql, req.Args) + + if err != nil { + return errors.WithStack(err) + } + + return httpx.OK(c, data) +} diff --git a/code/app/app-go/internal/handle/data_save.go b/code/app/app-go/internal/handle/data_save.go new file mode 100644 index 00000000..f550b99c --- /dev/null +++ b/code/app/app-go/internal/handle/data_save.go @@ -0,0 +1,56 @@ +package handle + +import ( + "allapp-go/internal/httpx" + "allapp-go/internal/types" + "allapp-go/pkg/db" + + "github.com/gofiber/fiber/v3" +) + +func SaveData(c fiber.Ctx) error { + var req []types.SaveDataReq + + if err := httpx.BindAndValidate(c, &req); err != nil { + return err + } + + if len(req) == 0 { + return httpx.OK(c, nil) + } + + ctx := c.Context() + dbClient := db.New() + + err := dbClient.WithTx(ctx, func(tx *db.Client) error { + for _, dataReq := range req { + tableName := dataReq.TableName + keyField := dataReq.KeyField + + for _, insert := range dataReq.Inserts { + if err := tx.Insert(ctx, tableName, keyField, insert); err != nil { + return err + } + } + + for _, update := range dataReq.Updates { + if err := tx.Update(ctx, tableName, keyField, update); err != nil { + return err + } + } + + for _, del := range dataReq.Deletes { + if err := tx.Delete(ctx, tableName, keyField, del); err != nil { + return err + } + } + } + return nil + }) + + if err != nil { + return err + } + + return httpx.OK(c, nil) +} diff --git a/code/app/app-go/internal/httpx/request.go b/code/app/app-go/internal/httpx/request.go new file mode 100644 index 00000000..5cb0f874 --- /dev/null +++ b/code/app/app-go/internal/httpx/request.go @@ -0,0 +1,20 @@ +package httpx + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v3" +) + +var validate = validator.New() + +func BindAndValidate(c fiber.Ctx, v any) error { + if err := c.Bind().Body(v); err != nil { + return err + } + + if err := validate.Struct(v); err != nil { + return err + } + + return nil +} diff --git a/code/app/app-go/internal/httpx/response.go b/code/app/app-go/internal/httpx/response.go new file mode 100644 index 00000000..ecf56958 --- /dev/null +++ b/code/app/app-go/internal/httpx/response.go @@ -0,0 +1,63 @@ +package httpx + +import "github.com/gofiber/fiber/v3" + +type Result struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` + TrackId string `json:"track_id,omitempty"` +} + +var BizCode = struct { + Success int // 成功 + Unauthorized int // 未授权/Token失效 + BusinessError int // 业务异常,例如参数错误、操作不允许 + SystemError int // 系统异常,例如数据库崩溃、未知错误 +}{ + Success: 1000, + Unauthorized: 1001, + BusinessError: 2000, + SystemError: 3000, +} + +// ------------------- 统一响应方法 ------------------- + +// Respond 返回通用响应,code和msg必填,data可选 +func Respond(c fiber.Ctx, code int, msg string, data interface{}) error { + trackId := "" + if v := c.Context().Value("request_id"); v != nil { + if s, ok := v.(string); ok { + trackId = s + } + } + + return c.JSON(Result{ + Code: code, + Message: msg, + Data: data, + TrackId: trackId, + }) +} + +// ------------------- 便捷封装 ------------------- + +// OK 成功响应 +func OK(c fiber.Ctx, data interface{}) error { + return Respond(c, BizCode.Success, "success", data) +} + +// Fail 业务异常 +func Fail(c fiber.Ctx, msg string) error { + return Respond(c, BizCode.BusinessError, msg, nil) +} + +// SystemError 系统异常 +func SystemError(c fiber.Ctx, msg string) error { + return Respond(c, BizCode.SystemError, msg, nil) +} + +// Unauthorized 授权失败 +func Unauthorized(c fiber.Ctx, msg string) error { + return Respond(c, BizCode.Unauthorized, msg, nil) +} diff --git a/code/app/app-go/internal/middleware/auth.go b/code/app/app-go/internal/middleware/auth.go new file mode 100644 index 00000000..c8e9a9f2 --- /dev/null +++ b/code/app/app-go/internal/middleware/auth.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "allapp-go/internal/httpx" + "allapp-go/pkg/jwtx" + "strings" + + "github.com/gofiber/fiber/v3" +) + +const CtxUserIDKey = "user_id" + +// Auth 登录校验中间件 +func Auth() fiber.Handler { + return func(c fiber.Ctx) error { + token := strings.TrimSpace(c.Get("Token")) + + if token == "" { + return httpx.Unauthorized(c, "未登录") + } + + verifyToken, ok := jwtx.VerifyToken(c.Context(), token) + if !ok { + return httpx.Unauthorized(c, "登录已过期") + } + + c.Locals(CtxUserIDKey, verifyToken.Data["user_id"]) + + return c.Next() + } +} + +func GetUserID(c fiber.Ctx) (int64, error) { + v := c.Locals(CtxUserIDKey) + + if v == nil { + return 0, httpx.Unauthorized(c, "登录已过期") + } + + userID, ok := v.(int64) + + if !ok { + return 0, httpx.Unauthorized(c, "登录已过期") + } + + return userID, nil +} diff --git a/code/app/app-go/internal/middleware/logger.go b/code/app/app-go/internal/middleware/logger.go new file mode 100644 index 00000000..1490fb73 --- /dev/null +++ b/code/app/app-go/internal/middleware/logger.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "allapp-go/pkg/logger" + "time" + + "github.com/gofiber/fiber/v3" + "go.uber.org/zap" +) + +func Logger() fiber.Handler { + return func(c fiber.Ctx) error { + traceID := c.RequestID() + + logger.SetTraceID(c, traceID) + + start := time.Now() + + err := c.Next() + + // ✅ 计算耗时 + cost := time.Since(start) + + log := logger.FromCtx(c.Context()) + log.Info("HTTP 请求", + zap.String("method", c.Method()), + zap.String("path", c.Path()), + zap.Int("status", c.Response().StatusCode()), + zap.String("ip", c.IP()), + zap.String("duration", cost.String()), + ) + + return err + } +} diff --git a/code/app/app-go/internal/router/router.go b/code/app/app-go/internal/router/router.go new file mode 100644 index 00000000..57f3fae7 --- /dev/null +++ b/code/app/app-go/internal/router/router.go @@ -0,0 +1,14 @@ +package router + +import ( + "allapp-go/internal/handle" + + "github.com/gofiber/fiber/v3" +) + +func SetupRouter(app *fiber.App) { + // ==================== data ==================== + app.Post("/data/loadData", handle.LoadData) + app.Post("/data/loadDataBySql", handle.LoadDataBySql) + app.Post("/data/saveData", handle.SaveData) +} diff --git a/code/app/app-go/internal/types/data_list.go b/code/app/app-go/internal/types/data_list.go new file mode 100644 index 00000000..5d5bdf7e --- /dev/null +++ b/code/app/app-go/internal/types/data_list.go @@ -0,0 +1,14 @@ +package types + +type LoadDataReq struct { + ViewName string `json:"view_name" validate:"required"` + SearchCondition string `json:"search_condition"` + OrderBy string `json:"order_by"` + SearchColumns []string `json:"search_columns"` + Args []any `json:"args"` +} + +type LoadDataBySqlReq struct { + Sql string `json:"sql" validate:"required"` + Args []any `json:"args"` +} diff --git a/code/app/app-go/internal/types/data_save.go b/code/app/app-go/internal/types/data_save.go new file mode 100644 index 00000000..d11e12b3 --- /dev/null +++ b/code/app/app-go/internal/types/data_save.go @@ -0,0 +1,9 @@ +package types + +type SaveDataReq struct { + TableName string `json:"table_name"` + KeyField string `json:"key_field"` + Inserts []map[string]any `json:"inserts"` + Updates []map[string]any `json:"updates"` + Deletes []map[string]any `json:"deletes"` +} diff --git a/code/app/app-go/pkg/db/common.go b/code/app/app-go/pkg/db/common.go new file mode 100644 index 00000000..6816b11e --- /dev/null +++ b/code/app/app-go/pkg/db/common.go @@ -0,0 +1,52 @@ +package db + +import ( + "fmt" + "strconv" + "strings" +) + +func validateData(table string, keyField string, data map[string]any) (any, error) { + if table == "" { + return nil, fmt.Errorf("表名不能为空") + } + + if len(data) == 0 { + return nil, fmt.Errorf("数据不能为空") + } + + if keyField == "" { + return nil, fmt.Errorf("主键字段不能为空") + } + + val, ok := data[keyField] + if !ok { + return nil, fmt.Errorf("缺少主键字段: %s", keyField) + } + + if val == nil { + return nil, fmt.Errorf("主键字段值不能为空") + } + + return val, nil +} + +// 高效 placeholder 转换 +func convertPlaceholder(sql string) string { + var sb strings.Builder + sb.Grow(len(sql)) + + argIndex := 1 + + for i := 0; i < len(sql); i++ { + if sql[i] == '?' { + sb.WriteByte('$') + sb.WriteString(strconv.Itoa(argIndex)) + argIndex++ + } else { + sb.WriteByte(sql[i]) + } + } + + return sb.String() +} diff --git a/code/app/app-go/pkg/db/db.go b/code/app/app-go/pkg/db/db.go new file mode 100644 index 00000000..d0e70395 --- /dev/null +++ b/code/app/app-go/pkg/db/db.go @@ -0,0 +1,42 @@ +package db + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" +) + +var defaultClient *Client + +type Executor interface { + Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) + Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) + QueryRow(ctx context.Context, sql string, args ...any) pgx.Row +} + +type Client struct { + pool *pgxpool.Pool + tx pgx.Tx +} + +// Init 初始化 +func Init(pool *pgxpool.Pool) { + defaultClient = &Client{pool: pool} +} + +// New 创建会话 +func New() *Client { + return &Client{ + pool: defaultClient.pool, + } +} + +// 内部获取执行器(关键) +func (c *Client) exec() Executor { + if c.tx != nil { + return c.tx + } + return c.pool +} diff --git a/code/app/app-go/pkg/db/delete.go b/code/app/app-go/pkg/db/delete.go new file mode 100644 index 00000000..a74bd51f --- /dev/null +++ b/code/app/app-go/pkg/db/delete.go @@ -0,0 +1,83 @@ +package db + +import ( + "context" + "fmt" + "strings" +) + +func (c *Client) Delete( + ctx context.Context, + table string, + keyField string, + data map[string]any, +) error { + if keyField == "" { + return fmt.Errorf("keyField 不能为空") + } + + keyVal, err := validateData(table, keyField, data) + if err != nil { + return err + } + + sql := fmt.Sprintf( + "DELETE FROM %s WHERE %s = $1", + table, + keyField, + ) + + _, err = c.exec().Exec(ctx, sql, keyVal) + return err +} + +func (c *Client) BatchDelete( + ctx context.Context, + table string, + keyField string, + dataList []map[string]any, +) error { + + if table == "" { + return fmt.Errorf("表名不能为空") + } + + if keyField == "" { + return fmt.Errorf("主键字段不能为空") + } + + if len(dataList) == 0 { + return fmt.Errorf("数据不能为空") + } + + var ( + placeholders []string + args []any + argIndex = 1 + ) + + for _, data := range dataList { + keyVal, err := validateData(table, keyField, data) + if err != nil { + return err + } + + if keyVal == nil { + return fmt.Errorf("主键字段值不能为空") + } + + placeholders = append(placeholders, fmt.Sprintf("$%d", argIndex)) + args = append(args, keyVal) + argIndex++ + } + + sql := fmt.Sprintf( + "DELETE FROM %s WHERE %s IN (%s)", + table, + keyField, + strings.Join(placeholders, ", "), + ) + + _, err := c.exec().Exec(ctx, sql, args...) + return err +} diff --git a/code/app/app-go/pkg/db/insert.go b/code/app/app-go/pkg/db/insert.go new file mode 100644 index 00000000..c56b5d49 --- /dev/null +++ b/code/app/app-go/pkg/db/insert.go @@ -0,0 +1,120 @@ +package db + +import ( + "context" + "fmt" + "sort" + "strings" +) + +// 构建 INSERT SQL(单条) +func buildInsertSQL(table string, data map[string]any) (string, []any) { + var ( + columns []string + placeholders []string + args []any + ) + + i := 1 + for col, val := range data { + columns = append(columns, col) + placeholders = append(placeholders, fmt.Sprintf("$%d", i)) + args = append(args, val) + i++ + } + + sql := fmt.Sprintf( + "INSERT INTO %s (%s) VALUES (%s)", + table, + strings.Join(columns, ", "), + strings.Join(placeholders, ", "), + ) + + return sql, args +} + +func (c *Client) Insert( + ctx context.Context, + table string, + keyField string, + data map[string]any, +) error { + if _, err := validateData(table, keyField, data); err != nil { + return err + } + + sql, args := buildInsertSQL(table, data) + + _, err := c.exec().Exec(ctx, sql, args...) + return err +} + +func (c *Client) BatchInsert( + ctx context.Context, + table string, + keyField string, + dataList []map[string]any, +) error { + if len(dataList) == 0 { + return fmt.Errorf("数据不能为空") + } + + // 用第一条确定列顺序 + first := dataList[0] + if _, err := validateData(table, keyField, first); err != nil { + return err + } + + var columns []string + for col := range first { + columns = append(columns, col) + } + + sort.Strings(columns) + + var ( + valueStrings []string + args []any + argIndex = 1 + ) + + for _, data := range dataList { + // 统一校验 + if _, err := validateData(table, keyField, data); err != nil { + return err + } + + // 字段数量检查 + if len(data) != len(columns) { + return fmt.Errorf("批量插入失败:数据字段不一致") + } + + var placeholders []string + + for _, col := range columns { + val, ok := data[col] + if !ok { + return fmt.Errorf("缺少字段: %s", col) + } + + placeholders = append(placeholders, fmt.Sprintf("$%d", argIndex)) + args = append(args, val) + argIndex++ + } + + valueStrings = append( + valueStrings, + fmt.Sprintf("(%s)", strings.Join(placeholders, ", ")), + ) + } + + sql := fmt.Sprintf( + "INSERT INTO %s (%s) VALUES %s", + table, + strings.Join(columns, ", "), + strings.Join(valueStrings, ", "), + ) + + _, err := c.exec().Exec(ctx, sql, args...) + return err +} diff --git a/code/app/app-go/pkg/db/select.go b/code/app/app-go/pkg/db/select.go new file mode 100644 index 00000000..41f29cb1 --- /dev/null +++ b/code/app/app-go/pkg/db/select.go @@ -0,0 +1,82 @@ +package db + +import ( + "context" + "fmt" + "strings" +) + +func (c *Client) LoadData( + ctx context.Context, + viewName string, + searchCondition string, + orderBy string, + searchColumns []string, + args []any, +) ([][]any, error) { + + var where string + + // WHERE 构造 + if searchCondition != "" && len(searchColumns) > 0 { + var conditions []string + + for _, col := range searchColumns { + conditions = append(conditions, fmt.Sprintf("%s LIKE ?", col)) + } + + where = " WHERE (" + strings.Join(conditions, " OR ") + ")" + + // 自动加 % + for i := range args { + if s, ok := args[i].(string); ok { + args[i] = "%" + s + "%" + } + } + } + + // ORDER BY + var order string + if orderBy != "" { + order = " ORDER BY " + orderBy + } + + sql := fmt.Sprintf("SELECT * FROM %s%s%s", viewName, where, order) + + return c.query(ctx, sql, args...) +} + +func (c *Client) LoadDataBySQL( + ctx context.Context, + sql string, + args []any, +) ([][]any, error) { + return c.query(ctx, sql, args...) +} + +// 统一查询方法(核心优化) +func (c *Client) query(ctx context.Context, sql string, args ...any) ([][]any, error) { + sql = convertPlaceholder(sql) + + rows, err := c.exec().Query(ctx, sql, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + result := make([][]any, 0, 16) + + for rows.Next() { + values, err := rows.Values() + if err != nil { + return nil, err + } + + row := make([]any, len(values)) + copy(row, values) + + result = append(result, row) + } + + return result, rows.Err() +} diff --git a/code/app/app-go/pkg/db/tx.go b/code/app/app-go/pkg/db/tx.go new file mode 100644 index 00000000..974fcd30 --- /dev/null +++ b/code/app/app-go/pkg/db/tx.go @@ -0,0 +1,34 @@ +package db + +import ( + "context" + + "github.com/jackc/pgx/v5" +) + +// WithTx 开启事务(在当前 client 上) +func (c *Client) WithTx(ctx context.Context, fn func(tx *Client) error) error { + tx, err := c.pool.Begin(ctx) + if err != nil { + return err + } + + // 创建事务 client + txClient := &Client{ + pool: c.pool, + tx: tx, + } + + defer func(tx pgx.Tx, ctx context.Context) { + err := tx.Rollback(ctx) + if err != nil { + + } + }(tx, ctx) + + if err := fn(txClient); err != nil { + return err + } + + return tx.Commit(ctx) +} diff --git a/code/app/app-go/pkg/db/update.go b/code/app/app-go/pkg/db/update.go new file mode 100644 index 00000000..01b06a03 --- /dev/null +++ b/code/app/app-go/pkg/db/update.go @@ -0,0 +1,140 @@ +package db + +import ( + "context" + "fmt" + "sort" + "strings" +) + +func (c *Client) Update( + ctx context.Context, + table string, + keyField string, + data map[string]any, +) error { + keyVal, err := validateData(table, keyField, data) + if err != nil { + return err + } + + var ( + setClauses []string + args []any + i = 1 + ) + + for col, val := range data { + if col == keyField { + continue + } + setClauses = append(setClauses, fmt.Sprintf("%s=$%d", col, i)) + args = append(args, val) + i++ + } + + // WHERE 条件 + where := fmt.Sprintf("%s=$%d", keyField, i) + args = append(args, keyVal) + + sql := fmt.Sprintf( + "UPDATE %s SET %s WHERE %s", + table, + strings.Join(setClauses, ", "), + where, + ) + + _, err = c.exec().Exec(ctx, sql, args...) + return err +} + +func (c *Client) BatchUpdate( + ctx context.Context, + table string, + keyField string, + dataList []map[string]any, +) error { + + if len(dataList) == 0 { + return fmt.Errorf("数据不能为空") + } + + // 用第一条数据确定字段 + first := dataList[0] + if _, err := validateData(table, keyField, first); err != nil { + return err + } + + // 提取字段(排除主键)+ 排序(关键) + var columns []string + for col := range first { + if col != keyField { + columns = append(columns, col) + } + } + sort.Strings(columns) + + var ( + args []any + argIndex = 1 + ) + + // CASE 语句 + var setClauses []string + + for _, col := range columns { + var caseBuilder strings.Builder + + caseBuilder.WriteString(fmt.Sprintf("%s = CASE %s ", col, keyField)) + + for _, data := range dataList { + // 校验 key + keyVal, err := validateData(table, keyField, data) + if err != nil { + return err + } + + val, ok := data[col] + if !ok { + return fmt.Errorf("缺少字段: %s", col) + } + + caseBuilder.WriteString(fmt.Sprintf( + "WHEN $%d THEN $%d ", + argIndex, + argIndex+1, + )) + + args = append(args, keyVal, val) + argIndex += 2 + } + + caseBuilder.WriteString("END") + setClauses = append(setClauses, caseBuilder.String()) + } + + // WHERE IN + var wherePlaceholders []string + + for _, data := range dataList { + keyVal, ok := data[keyField] + if !ok || keyVal == nil { + return fmt.Errorf("缺少主键字段值: %s", keyField) + } + + wherePlaceholders = append(wherePlaceholders, fmt.Sprintf("$%d", argIndex)) + args = append(args, keyVal) + argIndex++ + } + + sql := fmt.Sprintf( + "UPDATE %s SET %s WHERE %s IN (%s)", + table, + strings.Join(setClauses, ", "), + keyField, + strings.Join(wherePlaceholders, ", "), + ) + + _, err := c.exec().Exec(ctx, sql, args...) + return err +} diff --git a/code/app/app-go/pkg/jwtx/jwtx.go b/code/app/app-go/pkg/jwtx/jwtx.go new file mode 100644 index 00000000..83c1d46f --- /dev/null +++ b/code/app/app-go/pkg/jwtx/jwtx.go @@ -0,0 +1,112 @@ +package jwtx + +import ( + "context" + "sync" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// ================== +// 内部管理器 +// ================== + +type jwtManager struct { + secret string + tokenExpiry time.Duration +} + +var ( + manager *jwtManager + once sync.Once +) + +// ================== +// 初始化 +// ================== + +// Init 初始化(只执行一次) +// expiry = -1 表示不过期 +func Init(secret string, expiry time.Duration) { + once.Do(func() { + manager = &jwtManager{ + secret: secret, + tokenExpiry: expiry, + } + }) +} + +func getManager() *jwtManager { + if manager == nil { + panic("jwt未初始化,请先调用Init") + } + return manager +} + +// ================== +// Claims(通用结构) +// ================== + +type CustomClaims struct { + Data map[string]any `json:"data"` + jwt.RegisteredClaims +} + +// ================== +// 对外方法 +// ================== + +// CreateToken 生成 JWT +func CreateToken(ctx context.Context, data map[string]any) (string, error) { + m := getManager() + return m.createToken(data) +} + +// VerifyToken 校验 token 并返回 claims +func VerifyToken(ctx context.Context, tokenString string) (*CustomClaims, bool) { + if tokenString == "" { + return nil, false + } + + m := getManager() + + token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(m.secret), nil + }) + + if err != nil || !token.Valid { + return nil, false + } + + claims, ok := token.Claims.(*CustomClaims) + if !ok { + return nil, false + } + + return claims, true +} + +// ================== +// 内部实现 +// ================== + +func (j *jwtManager) createToken(data map[string]any) (string, error) { + now := time.Now() + + claims := &CustomClaims{ + Data: data, + RegisteredClaims: jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(now), + }, + } + + // ✅ 只有 >0 才设置过期时间 + // -1 / 0 都表示不过期 + if j.tokenExpiry > 0 { + claims.ExpiresAt = jwt.NewNumericDate(now.Add(j.tokenExpiry)) + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(j.secret)) +} diff --git a/code/app/app-go/pkg/logger/logger.go b/code/app/app-go/pkg/logger/logger.go new file mode 100644 index 00000000..06643e30 --- /dev/null +++ b/code/app/app-go/pkg/logger/logger.go @@ -0,0 +1,233 @@ +package logger + +import ( + "context" + "os" + "path/filepath" + "strings" + "time" + + "github.com/gofiber/fiber/v3" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +var Log *zap.Logger + +const TraceIDKey = "request_id" + +// ====================== +// 配置结构 +// ====================== + +type Config struct { + Dir string // 日志目录 + Level string // debug info warn error + MaxSize int // MB + MaxBackups int + MaxAge int // 天 + Compress bool // 是否压缩 +} + +// ====================== +// 初始化 +// ====================== + +func Init(cfg Config) { + if cfg.Dir == "" { + cfg.Dir = "logs" + } + + if cfg.Level == "" { + cfg.Level = "info" + } + + level := parseLevel(cfg.Level) + + // ====================== + // encoder 配置 + // ====================== + + encoderConfig := zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + MessageKey: "msg", + CallerKey: "caller", + EncodeCaller: zapcore.ShortCallerEncoder, + } + + // 👉 文件 JSON encoder + jsonEncoder := zapcore.NewJSONEncoder(func() zapcore.EncoderConfig { + cfg := encoderConfig + cfg.EncodeLevel = zapcore.CapitalLevelEncoder + cfg.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05") + return cfg + }()) + + // 👉 控制台 encoder(可读) + consoleEncoder := zapcore.NewConsoleEncoder(func() zapcore.EncoderConfig { + cfg := encoderConfig + cfg.EncodeLevel = zapcore.CapitalColorLevelEncoder + cfg.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05") + return cfg + }()) + + // ====================== + // writer + // ====================== + + consoleWriter := zapcore.AddSync(os.Stdout) + infoWriter := getWriter(cfg, "info") + errorWriter := getWriter(cfg, "error") + + // ====================== + // core + // ====================== + + // 控制台(所有级别) + consoleCore := zapcore.NewCore( + consoleEncoder, + consoleWriter, + level, + ) + + // info 文件 + infoCore := zapcore.NewCore( + jsonEncoder, + infoWriter, + zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl < zapcore.ErrorLevel && lvl >= level + }), + ) + + // error 文件 + errorCore := zapcore.NewCore( + jsonEncoder, + errorWriter, + zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zapcore.ErrorLevel + }), + ) + + core := zapcore.NewTee( + consoleCore, + infoCore, + errorCore, + ) + + Log = zap.New( + core, + zap.AddCaller(), // 打印调用位置 + zap.AddCallerSkip(1), // 跳过封装层 + ) +} + +// ====================== +// Daily Writer(按天切割) +// ====================== + +type DailyWriter struct { + cfg Config + level string + log *lumberjack.Logger + date string +} + +func newDailyWriter(cfg Config, level string) *DailyWriter { + return &DailyWriter{ + cfg: cfg, + level: level, + } +} + +func (w *DailyWriter) Write(p []byte) (n int, err error) { + today := time.Now().Format("2006-01-02") + + if w.log == nil || w.date != today { + dir := filepath.Join(w.cfg.Dir, w.level) + _ = os.MkdirAll(dir, os.ModePerm) + + filename := filepath.Join(dir, today+".log") + + w.log = &lumberjack.Logger{ + Filename: filename, + MaxSize: w.cfg.MaxSize, + MaxBackups: w.cfg.MaxBackups, + MaxAge: w.cfg.MaxAge, + Compress: w.cfg.Compress, + } + + w.date = today + } + + return w.log.Write(p) +} + +func (w *DailyWriter) Sync() error { + if w.log != nil { + return w.log.Close() + } + return nil +} + +// ====================== +// writer 封装 +// ====================== + +func getWriter(cfg Config, level string) zapcore.WriteSyncer { + return zapcore.AddSync(newDailyWriter(cfg, level)) +} + +// ====================== +// level 解析 +// ====================== + +func parseLevel(lvl string) zapcore.Level { + switch strings.ToLower(lvl) { + case "debug": + return zap.DebugLevel + case "warn": + return zap.WarnLevel + case "error": + return zap.ErrorLevel + default: + return zap.InfoLevel + } +} + +// ====================== +// trace 支持 +// ====================== + +func SetTraceID(c fiber.Ctx, traceID string) { + if c == nil { + return + } + + ctx := context.WithValue(c.Context(), TraceIDKey, traceID) + c.SetContext(ctx) +} + +func FromCtx(ctx context.Context) *zap.Logger { + if ctx == nil { + return Log + } + + traceID, _ := ctx.Value(TraceIDKey).(string) + if traceID == "" { + return Log + } + + return Log.With(zap.String("trace_id", traceID)) +} + +// ====================== +// 优雅退出 +// ====================== + +func Sync() { + if Log != nil { + _ = Log.Sync() + } +} diff --git a/code/app/app-go/pkg/uniqueid/DefaultIdGenerator.go b/code/app/app-go/pkg/uniqueid/DefaultIdGenerator.go new file mode 100644 index 00000000..3d2d57f4 --- /dev/null +++ b/code/app/app-go/pkg/uniqueid/DefaultIdGenerator.go @@ -0,0 +1,83 @@ +package uniqueid + +import ( + "strconv" + "time" +) + +type DefaultIdGenerator struct { + Options *IdGeneratorOptions + SnowWorker ISnowWorker + IdGeneratorException IdGeneratorException +} + +func NewDefaultIdGenerator(options *IdGeneratorOptions) *DefaultIdGenerator { + if options == nil { + panic("dig.Options error.") + } + + // 1.BaseTime + minTime := int64(631123200000) // time.Now().AddDate(-30, 0, 0).UnixNano() / 1e6 + if options.BaseTime < minTime || options.BaseTime > time.Now().UnixNano()/1e6 { + panic("BaseTime error.") + } + + // 2.WorkerIdBitLength + if options.WorkerIdBitLength <= 0 { + panic("WorkerIdBitLength error.(range:[1, 21])") + } + if options.WorkerIdBitLength+options.SeqBitLength > 22 { + panic("error:WorkerIdBitLength + SeqBitLength <= 22") + } + + // 3.WorkerId + maxWorkerIdNumber := uint16(1< maxWorkerIdNumber { + panic("WorkerId error. (range:[0, " + strconv.FormatUint(uint64(maxWorkerIdNumber), 10) + "]") + } + + // 4.SeqBitLength + if options.SeqBitLength < 2 || options.SeqBitLength > 21 { + panic("SeqBitLength error. (range:[2, 21])") + } + + // 5.MaxSeqNumber + maxSeqNumber := uint32(1< maxSeqNumber { + panic("MaxSeqNumber error. (range:[1, " + strconv.FormatUint(uint64(maxSeqNumber), 10) + "]") + } + + // 6.MinSeqNumber + if options.MinSeqNumber < 5 || options.MinSeqNumber > maxSeqNumber { + panic("MinSeqNumber error. (range:[5, " + strconv.FormatUint(uint64(maxSeqNumber), 10) + "]") + } + + var snowWorker ISnowWorker + switch options.Method { + case 1: + snowWorker = NewSnowWorkerM1(options) + case 2: + snowWorker = NewSnowWorkerM2(options) + default: + snowWorker = NewSnowWorkerM1(options) + } + + if options.Method == 1 { + time.Sleep(time.Duration(500) * time.Microsecond) + } + + return &DefaultIdGenerator{ + Options: options, + SnowWorker: snowWorker, + } +} + +func (dig DefaultIdGenerator) NewLong() int64 { + return dig.SnowWorker.NextId() +} diff --git a/code/app/app-go/pkg/uniqueid/IIdGenerator.go b/code/app/app-go/pkg/uniqueid/IIdGenerator.go new file mode 100644 index 00000000..a0c1e4de --- /dev/null +++ b/code/app/app-go/pkg/uniqueid/IIdGenerator.go @@ -0,0 +1,5 @@ +package uniqueid + +type IIdGenerator interface { + NewLong() uint64 +} diff --git a/code/app/app-go/pkg/uniqueid/ISnowWorker.go b/code/app/app-go/pkg/uniqueid/ISnowWorker.go new file mode 100644 index 00000000..c066e6d6 --- /dev/null +++ b/code/app/app-go/pkg/uniqueid/ISnowWorker.go @@ -0,0 +1,5 @@ +package uniqueid + +type ISnowWorker interface { + NextId() int64 +} diff --git a/code/app/app-go/pkg/uniqueid/IdGeneratorException.go b/code/app/app-go/pkg/uniqueid/IdGeneratorException.go new file mode 100644 index 00000000..c0ae9086 --- /dev/null +++ b/code/app/app-go/pkg/uniqueid/IdGeneratorException.go @@ -0,0 +1,12 @@ +package uniqueid + +import "fmt" + +type IdGeneratorException struct { + message string + error error +} + +func (e IdGeneratorException) IdGeneratorException(message ...interface{}) { + fmt.Println(message) +} diff --git a/code/app/app-go/pkg/uniqueid/IdGeneratorOptions.go b/code/app/app-go/pkg/uniqueid/IdGeneratorOptions.go new file mode 100644 index 00000000..1f4ff6e1 --- /dev/null +++ b/code/app/app-go/pkg/uniqueid/IdGeneratorOptions.go @@ -0,0 +1,25 @@ +package uniqueid + +type IdGeneratorOptions struct { + Method uint16 // 雪花计算方法,(1-漂移算法|2-传统算法),默认1 + BaseTime int64 // 基础时间(ms单位),不能超过当前系统时间 + WorkerId uint16 // 机器码,必须由外部设定,最大值 2^WorkerIdBitLength-1 + WorkerIdBitLength byte // 机器码位长,默认值6,取值范围 [1, 15](要求:序列数位长+机器码位长不超过22) + SeqBitLength byte // 序列数位长,默认值6,取值范围 [3, 21](要求:序列数位长+机器码位长不超过22) + MaxSeqNumber uint32 // 最大序列数(含),设置范围 [MinSeqNumber, 2^SeqBitLength-1],默认值0,表示最大序列数取最大值(2^SeqBitLength-1]) + MinSeqNumber uint32 // 最小序列数(含),默认值5,取值范围 [5, MaxSeqNumber],每毫秒的前5个序列数对应编号0-4是保留位,其中1-4是时间回拨相应预留位,0是手工新值预留位 + TopOverCostCount uint32 // 最大漂移次数(含),默认2000,推荐范围500-10000(与计算能力有关) +} + +func NewIdGeneratorOptions(workerId uint16) *IdGeneratorOptions { + return &IdGeneratorOptions{ + Method: 1, + WorkerId: workerId, + BaseTime: 1582136402000, + WorkerIdBitLength: 6, + SeqBitLength: 6, + MaxSeqNumber: 0, + MinSeqNumber: 5, + TopOverCostCount: 2000, + } +} diff --git a/code/app/app-go/pkg/uniqueid/IdHelper.go b/code/app/app-go/pkg/uniqueid/IdHelper.go new file mode 100644 index 00000000..a76494a8 --- /dev/null +++ b/code/app/app-go/pkg/uniqueid/IdHelper.go @@ -0,0 +1,29 @@ +package uniqueid + +import ( + "sync" +) + +var singletonMutex sync.Mutex +var idGenerator *DefaultIdGenerator + +// SetIdGenerator . +func SetIdGenerator(options *IdGeneratorOptions) { + singletonMutex.Lock() + idGenerator = NewDefaultIdGenerator(options) + singletonMutex.Unlock() +} + +// NextId . +func NextId() int64 { + if idGenerator == nil { + singletonMutex.Lock() + defer singletonMutex.Unlock() + if idGenerator == nil { + options := NewIdGeneratorOptions(1) + idGenerator = NewDefaultIdGenerator(options) + } + } + + return idGenerator.NewLong() +} diff --git a/code/app/app-go/pkg/uniqueid/OverCostActionArg.go b/code/app/app-go/pkg/uniqueid/OverCostActionArg.go new file mode 100644 index 00000000..5b8942cc --- /dev/null +++ b/code/app/app-go/pkg/uniqueid/OverCostActionArg.go @@ -0,0 +1,19 @@ +package uniqueid + +type OverCostActionArg struct { + ActionType int32 + TimeTick int64 + WorkerId uint16 + OverCostCountInOneTerm int32 + GenCountInOneTerm int32 + TermIndex int32 +} + +func (ocaa OverCostActionArg) OverCostActionArg(workerId uint16, timeTick int64, actionType int32, overCostCountInOneTerm int32, genCountWhenOverCost int32, index int32) { + ocaa.ActionType = actionType + ocaa.TimeTick = timeTick + ocaa.WorkerId = workerId + ocaa.OverCostCountInOneTerm = overCostCountInOneTerm + ocaa.GenCountInOneTerm = genCountWhenOverCost + ocaa.TermIndex = index +} diff --git a/code/app/app-go/pkg/uniqueid/SnowWorkerM1.go b/code/app/app-go/pkg/uniqueid/SnowWorkerM1.go new file mode 100644 index 00000000..b9edde7f --- /dev/null +++ b/code/app/app-go/pkg/uniqueid/SnowWorkerM1.go @@ -0,0 +1,243 @@ +package uniqueid + +import ( + "sync" + "time" +) + +// SnowWorkerM1 . +type SnowWorkerM1 struct { + BaseTime int64 //基础时间 + WorkerId uint16 //机器码 + WorkerIdBitLength byte //机器码位长 + SeqBitLength byte //自增序列数位长 + MaxSeqNumber uint32 //最大序列数(含) + MinSeqNumber uint32 //最小序列数(含) + TopOverCostCount uint32 //最大漂移次数 + _TimestampShift byte + _CurrentSeqNumber uint32 + + _LastTimeTick int64 + _TurnBackTimeTick int64 + _TurnBackIndex byte + _IsOverCost bool + _OverCostCountInOneTerm uint32 + _GenCountInOneTerm uint32 + _TermIndex uint32 + + sync.Mutex +} + +// NewSnowWorkerM1 . +func NewSnowWorkerM1(options *IdGeneratorOptions) ISnowWorker { + var workerIdBitLength byte + var seqBitLength byte + var maxSeqNumber uint32 + + // 1.BaseTime + var baseTime int64 + if options.BaseTime != 0 { + baseTime = options.BaseTime + } else { + baseTime = 1582136402000 + } + + // 2.WorkerIdBitLength + if options.WorkerIdBitLength == 0 { + workerIdBitLength = 6 + } else { + workerIdBitLength = options.WorkerIdBitLength + } + + // 3.WorkerId + var workerId = options.WorkerId + + // 4.SeqBitLength + if options.SeqBitLength == 0 { + seqBitLength = 6 + } else { + seqBitLength = options.SeqBitLength + } + + // 5.MaxSeqNumber + if options.MaxSeqNumber <= 0 { + maxSeqNumber = (1 << seqBitLength) - 1 + } else { + maxSeqNumber = options.MaxSeqNumber + } + + // 6.MinSeqNumber + var minSeqNumber = options.MinSeqNumber + + // 7.Others + var topOverCostCount = options.TopOverCostCount + if topOverCostCount == 0 { + topOverCostCount = 2000 + } + + timestampShift := (byte)(workerIdBitLength + seqBitLength) + currentSeqNumber := minSeqNumber + + return &SnowWorkerM1{ + BaseTime: baseTime, + WorkerIdBitLength: workerIdBitLength, + WorkerId: workerId, + SeqBitLength: seqBitLength, + MaxSeqNumber: maxSeqNumber, + MinSeqNumber: minSeqNumber, + TopOverCostCount: topOverCostCount, + _TimestampShift: timestampShift, + _CurrentSeqNumber: currentSeqNumber, + + _LastTimeTick: 0, + _TurnBackTimeTick: 0, + _TurnBackIndex: 0, + _IsOverCost: false, + _OverCostCountInOneTerm: 0, + _GenCountInOneTerm: 0, + _TermIndex: 0, + } +} + +// DoGenIDAction . +func (m1 *SnowWorkerM1) DoGenIdAction(arg *OverCostActionArg) { + +} + +func (m1 *SnowWorkerM1) BeginOverCostAction(useTimeTick int64) { + +} + +func (m1 *SnowWorkerM1) EndOverCostAction(useTimeTick int64) { + if m1._TermIndex > 10000 { + m1._TermIndex = 0 + } +} + +func (m1 *SnowWorkerM1) BeginTurnBackAction(useTimeTick int64) { + +} + +func (m1 *SnowWorkerM1) EndTurnBackAction(useTimeTick int64) { + +} + +func (m1 *SnowWorkerM1) NextOverCostId() int64 { + currentTimeTick := m1.GetCurrentTimeTick() + if currentTimeTick > m1._LastTimeTick { + m1.EndOverCostAction(currentTimeTick) + m1._LastTimeTick = currentTimeTick + m1._CurrentSeqNumber = m1.MinSeqNumber + m1._IsOverCost = false + m1._OverCostCountInOneTerm = 0 + m1._GenCountInOneTerm = 0 + return m1.CalcId(m1._LastTimeTick) + } + if m1._OverCostCountInOneTerm >= m1.TopOverCostCount { + m1.EndOverCostAction(currentTimeTick) + m1._LastTimeTick = m1.GetNextTimeTick() + m1._CurrentSeqNumber = m1.MinSeqNumber + m1._IsOverCost = false + m1._OverCostCountInOneTerm = 0 + m1._GenCountInOneTerm = 0 + return m1.CalcId(m1._LastTimeTick) + } + if m1._CurrentSeqNumber > m1.MaxSeqNumber { + m1._LastTimeTick++ + m1._CurrentSeqNumber = m1.MinSeqNumber + m1._IsOverCost = true + m1._OverCostCountInOneTerm++ + m1._GenCountInOneTerm++ + + return m1.CalcId(m1._LastTimeTick) + } + + m1._GenCountInOneTerm++ + return m1.CalcId(m1._LastTimeTick) +} + +// NextNormalID . +func (m1 *SnowWorkerM1) NextNormalId() int64 { + currentTimeTick := m1.GetCurrentTimeTick() + if currentTimeTick < m1._LastTimeTick { + if m1._TurnBackTimeTick < 1 { + m1._TurnBackTimeTick = m1._LastTimeTick - 1 + m1._TurnBackIndex++ + // 每毫秒序列数的前5位是预留位,0用于手工新值,1-4是时间回拨次序 + // 最多4次回拨(防止回拨重叠) + if m1._TurnBackIndex > 4 { + m1._TurnBackIndex = 1 + } + m1.BeginTurnBackAction(m1._TurnBackTimeTick) + } + + // time.Sleep(time.Duration(1) * time.Millisecond) + return m1.CalcTurnBackId(m1._TurnBackTimeTick) + } + + // 时间追平时,_TurnBackTimeTick清零 + if m1._TurnBackTimeTick > 0 { + m1.EndTurnBackAction(m1._TurnBackTimeTick) + m1._TurnBackTimeTick = 0 + } + + if currentTimeTick > m1._LastTimeTick { + m1._LastTimeTick = currentTimeTick + m1._CurrentSeqNumber = m1.MinSeqNumber + return m1.CalcId(m1._LastTimeTick) + } + + if m1._CurrentSeqNumber > m1.MaxSeqNumber { + m1.BeginOverCostAction(currentTimeTick) + m1._TermIndex++ + m1._LastTimeTick++ + m1._CurrentSeqNumber = m1.MinSeqNumber + m1._IsOverCost = true + m1._OverCostCountInOneTerm = 1 + m1._GenCountInOneTerm = 1 + + return m1.CalcId(m1._LastTimeTick) + } + + return m1.CalcId(m1._LastTimeTick) +} + +// CalcID . +func (m1 *SnowWorkerM1) CalcId(useTimeTick int64) int64 { + result := int64(useTimeTick< m2.MaxSeqNumber { + m2._CurrentSeqNumber = m2.MinSeqNumber + currentTimeTick = m2.GetNextTimeTick() + } + } else { + m2._CurrentSeqNumber = m2.MinSeqNumber + } + if currentTimeTick < m2._LastTimeTick { + fmt.Println("Time error for {0} milliseconds", strconv.FormatInt(m2._LastTimeTick-currentTimeTick, 10)) + } + m2._LastTimeTick = currentTimeTick + result := int64(currentTimeTick< + }} + > - ); + ) } diff --git a/code/app/app-rn/app/(tabs)/explore.tsx b/code/app/app-rn/app/(tabs)/explore.tsx index 71518f9a..ab4b9e4d 100644 --- a/code/app/app-rn/app/(tabs)/explore.tsx +++ b/code/app/app-rn/app/(tabs)/explore.tsx @@ -1,44 +1,53 @@ -import { Image } from 'expo-image'; -import { Platform, StyleSheet } from 'react-native'; +import { Image } from 'expo-image' +import { Platform, StyleSheet } from 'react-native' -import { Collapsible } from '@/components/ui/collapsible'; -import { ExternalLink } from '@/components/external-link'; -import ParallaxScrollView from '@/components/parallax-scroll-view'; -import { ThemedText } from '@/components/themed-text'; -import { ThemedView } from '@/components/themed-view'; -import { IconSymbol } from '@/components/ui/icon-symbol'; -import { Fonts } from '@/constants/theme'; +import { ExternalLink } from '@/components/external-link' +import ParallaxScrollView from '@/components/parallax-scroll-view' +import { ThemedText } from '@/components/themed-text' +import { ThemedView } from '@/components/themed-view' +import { Collapsible } from '@/components/ui/collapsible' +import { IconSymbol } from '@/components/ui/icon-symbol' +import { Fonts } from '@/constants/theme' export default function TabTwoScreen() { return ( - }> + )} + > + }} + > Explore This app includes example code to help you get started. - This app has two screens:{' '} - app/(tabs)/index.tsx and{' '} + This app has two screens: + {' '} + app/(tabs)/index.tsx + {' '} + and + {' '} app/(tabs)/explore.tsx - The layout file in app/(tabs)/_layout.tsx{' '} + The layout file in + {' '} + app/(tabs)/_layout.tsx + {' '} sets up the tab navigator. @@ -47,14 +56,24 @@ export default function TabTwoScreen() { - You can open this project on Android, iOS, and the web. To open the web version, press{' '} - w in the terminal running this project. + You can open this project on Android, iOS, and the web. To open the web version, press + {' '} + w + {' '} + in the terminal running this project. - For static images, you can use the @2x and{' '} - @3x suffixes to provide files for + For static images, you can use the + {' '} + @2x + {' '} + and + {' '} + @3x + {' '} + suffixes to provide files for different screen densities - This template has light and dark mode support. The{' '} - useColorScheme() hook lets you inspect + This template has light and dark mode support. The + {' '} + useColorScheme() + {' '} + hook lets you inspect what the user's current color scheme is, and so you can adjust UI colors accordingly. @@ -77,25 +99,33 @@ export default function TabTwoScreen() { - This template includes an example of an animated component. The{' '} - components/HelloWave.tsx component uses - the powerful{' '} + This template includes an example of an animated component. The + {' '} + components/HelloWave.tsx + {' '} + component uses + the powerful + {' '} react-native-reanimated - {' '} + + {' '} library to create a waving hand animation. {Platform.select({ ios: ( - The components/ParallaxScrollView.tsx{' '} + The + {' '} + components/ParallaxScrollView.tsx + {' '} component provides a parallax effect for the header image. ), })} - ); + ) } const styles = StyleSheet.create({ @@ -109,4 +139,4 @@ const styles = StyleSheet.create({ flexDirection: 'row', gap: 8, }, -}); +}) diff --git a/code/app/app-rn/app/(tabs)/index.tsx b/code/app/app-rn/app/(tabs)/index.tsx index 786b7366..3c2a6170 100644 --- a/code/app/app-rn/app/(tabs)/index.tsx +++ b/code/app/app-rn/app/(tabs)/index.tsx @@ -1,98 +1,12 @@ -import { Image } from 'expo-image'; -import { Platform, StyleSheet } from 'react-native'; - -import { HelloWave } from '@/components/hello-wave'; -import ParallaxScrollView from '@/components/parallax-scroll-view'; -import { ThemedText } from '@/components/themed-text'; -import { ThemedView } from '@/components/themed-view'; -import { Link } from 'expo-router'; +import { Platform, StyleSheet, Text, View } from 'react-native' +import { Button, ButtonIcon, ButtonSpinner, ButtonText } from '@/components/ui/button' export default function HomeScreen() { return ( - - }> - - Welcome! - - - - Step 1: Try it - - Edit app/(tabs)/index.tsx to see changes. - Press{' '} - - {Platform.select({ - ios: 'cmd + d', - android: 'cmd + m', - web: 'F12', - })} - {' '} - to open developer tools. - - - - - - Step 2: Explore - - - - alert('Action pressed')} /> - alert('Share pressed')} - /> - - alert('Delete pressed')} - /> - - - - - - {`Tap the Explore tab to learn more about what's included in this starter app.`} - - - - Step 3: Get a fresh start - - {`When you're ready, run `} - npm run reset-project to get a fresh{' '} - app directory. This will move the current{' '} - app to{' '} - app-example. - - - - ); + + + + ) } - -const styles = StyleSheet.create({ - titleContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - stepContainer: { - gap: 8, - marginBottom: 8, - }, - reactLogo: { - height: 178, - width: 290, - bottom: 0, - left: 0, - position: 'absolute', - }, -}); diff --git a/code/app/app-rn/app/_layout.tsx b/code/app/app-rn/app/_layout.tsx index f518c9b0..beaf8ebe 100644 --- a/code/app/app-rn/app/_layout.tsx +++ b/code/app/app-rn/app/_layout.tsx @@ -1,24 +1,38 @@ -import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; -import { Stack } from 'expo-router'; -import { StatusBar } from 'expo-status-bar'; -import 'react-native-reanimated'; +import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native' +import { Stack } from 'expo-router' +import { StatusBar } from 'expo-status-bar' +import { useEffect } from 'react' +import { Appearance } from 'react-native' +import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider' +import { useAppStore } from '@/store/app' +import 'react-native-reanimated' -import { useColorScheme } from '@/hooks/use-color-scheme'; +import '@/styles/global.css' export const unstable_settings = { anchor: '(tabs)', -}; +} export default function RootLayout() { - const colorScheme = useColorScheme(); + const appStore = useAppStore() + + useEffect(() => { + const sub = Appearance.addChangeListener(() => { + useAppStore.getState().syncWithSystem() + }) + + return () => sub.remove() + }, []) return ( - - - - - - - - ); + + + + + + + + + + ) } diff --git a/code/app/app-rn/babel.config.js b/code/app/app-rn/babel.config.js index 9d89e131..b7d33e78 100644 --- a/code/app/app-rn/babel.config.js +++ b/code/app/app-rn/babel.config.js @@ -1,6 +1,9 @@ module.exports = function (api) { - api.cache(true); + api.cache(true) return { - presets: ['babel-preset-expo'], - }; -}; + presets: [ + ['babel-preset-expo', { jsxImportSource: 'nativewind' }], + 'nativewind/babel', + ], + } +} \ No newline at end of file diff --git a/code/app/app-rn/components/ui/button/index.tsx b/code/app/app-rn/components/ui/button/index.tsx new file mode 100644 index 00000000..0e2163cf --- /dev/null +++ b/code/app/app-rn/components/ui/button/index.tsx @@ -0,0 +1,436 @@ +'use client' +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils' +import { createButton } from '@gluestack-ui/core/button/creator' +import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator' +import { + tva, + useStyleContext, + + withStyleContext, +} from '@gluestack-ui/utils/nativewind-utils' +import { cssInterop } from 'nativewind' +import * as React from 'react' +import { ActivityIndicator, Pressable, Text, View } from 'react-native' + +const SCOPE = 'BUTTON' + +const Root = withStyleContext(Pressable, SCOPE) + +const UIButton = createButton({ + Root, + Text, + Group: View, + Spinner: ActivityIndicator, + Icon: UIIcon, +}) + +cssInterop(PrimitiveIcon, { + className: { + target: 'style', + nativeStyleToProp: { + height: true, + width: true, + fill: true, + color: 'classNameColor', + stroke: true, + }, + }, +}) + +const buttonStyle = tva({ + base: 'group/button rounded bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2', + variants: { + action: { + primary: + 'bg-primary-500 data-[hover=true]:bg-primary-600 data-[active=true]:bg-primary-700 border-primary-300 data-[hover=true]:border-primary-400 data-[active=true]:border-primary-500 data-[focus-visible=true]:web:ring-indicator-info', + secondary: + 'bg-secondary-500 border-secondary-300 data-[hover=true]:bg-secondary-600 data-[hover=true]:border-secondary-400 data-[active=true]:bg-secondary-700 data-[active=true]:border-secondary-700 data-[focus-visible=true]:web:ring-indicator-info', + positive: + 'bg-success-500 border-success-300 data-[hover=true]:bg-success-600 data-[hover=true]:border-success-400 data-[active=true]:bg-success-700 data-[active=true]:border-success-500 data-[focus-visible=true]:web:ring-indicator-info', + negative: + 'bg-error-500 border-error-300 data-[hover=true]:bg-error-600 data-[hover=true]:border-error-400 data-[active=true]:bg-error-700 data-[active=true]:border-error-500 data-[focus-visible=true]:web:ring-indicator-info', + default: + 'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', + }, + variant: { + link: 'px-0', + outline: + 'bg-transparent border data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', + solid: '', + }, + + size: { + xs: 'px-3.5 h-8', + sm: 'px-4 h-9', + md: 'px-5 h-10', + lg: 'px-6 h-11', + xl: 'px-7 h-12', + }, + }, + compoundVariants: [ + { + action: 'primary', + variant: 'link', + class: + 'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent', + }, + { + action: 'secondary', + variant: 'link', + class: + 'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent', + }, + { + action: 'positive', + variant: 'link', + class: + 'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent', + }, + { + action: 'negative', + variant: 'link', + class: + 'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent', + }, + { + action: 'primary', + variant: 'outline', + class: + 'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', + }, + { + action: 'secondary', + variant: 'outline', + class: + 'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', + }, + { + action: 'positive', + variant: 'outline', + class: + 'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', + }, + { + action: 'negative', + variant: 'outline', + class: + 'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent', + }, + ], +}) + +const buttonTextStyle = tva({ + base: 'text-typography-0 font-semibold web:select-none', + parentVariants: { + action: { + primary: + 'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700', + secondary: + 'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700', + positive: + 'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700', + negative: + 'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700', + }, + variant: { + link: 'data-[hover=true]:underline data-[active=true]:underline', + outline: '', + solid: + 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', + }, + size: { + xs: 'text-xs', + sm: 'text-sm', + md: 'text-base', + lg: 'text-lg', + xl: 'text-xl', + }, + }, + parentCompoundVariants: [ + { + variant: 'solid', + action: 'primary', + class: + 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', + }, + { + variant: 'solid', + action: 'secondary', + class: + 'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800', + }, + { + variant: 'solid', + action: 'positive', + class: + 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', + }, + { + variant: 'solid', + action: 'negative', + class: + 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', + }, + { + variant: 'outline', + action: 'primary', + class: + 'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500', + }, + { + variant: 'outline', + action: 'secondary', + class: + 'text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700', + }, + { + variant: 'outline', + action: 'positive', + class: + 'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500', + }, + { + variant: 'outline', + action: 'negative', + class: + 'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500', + }, + ], +}) + +const buttonIconStyle = tva({ + base: 'fill-none', + parentVariants: { + variant: { + link: 'data-[hover=true]:underline data-[active=true]:underline', + outline: '', + solid: + 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', + }, + size: { + xs: 'h-3.5 w-3.5', + sm: 'h-4 w-4', + md: 'h-[18px] w-[18px]', + lg: 'h-[18px] w-[18px]', + xl: 'h-5 w-5', + }, + action: { + primary: + 'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700', + secondary: + 'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700', + positive: + 'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700', + + negative: + 'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700', + }, + }, + parentCompoundVariants: [ + { + variant: 'solid', + action: 'primary', + class: + 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', + }, + { + variant: 'solid', + action: 'secondary', + class: + 'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800', + }, + { + variant: 'solid', + action: 'positive', + class: + 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', + }, + { + variant: 'solid', + action: 'negative', + class: + 'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0', + }, + ], +}) + +const buttonGroupStyle = tva({ + base: '', + variants: { + space: { + 'xs': 'gap-1', + 'sm': 'gap-2', + 'md': 'gap-3', + 'lg': 'gap-4', + 'xl': 'gap-5', + '2xl': 'gap-6', + '3xl': 'gap-7', + '4xl': 'gap-8', + }, + isAttached: { + true: 'gap-0', + }, + flexDirection: { + 'row': 'flex-row', + 'column': 'flex-col', + 'row-reverse': 'flex-row-reverse', + 'column-reverse': 'flex-col-reverse', + }, + }, +}) + +type IButtonProps = Omit< + React.ComponentPropsWithoutRef, + 'context' +> +& VariantProps & { className?: string } + +const Button = React.forwardRef< + React.ElementRef, + IButtonProps +>( + ( + { className, variant = 'solid', size = 'md', action = 'primary', ...props }, + ref, + ) => { + return ( + + ) + }, +) + +type IButtonTextProps = React.ComponentPropsWithoutRef + & VariantProps & { className?: string } + +const ButtonText = React.forwardRef< + React.ElementRef, + IButtonTextProps +>(({ className, variant, size, action, ...props }, ref) => { + const { + variant: parentVariant, + size: parentSize, + action: parentAction, + } = useStyleContext(SCOPE) + + return ( + + ) +}) + +const ButtonSpinner = UIButton.Spinner + +type IButtonIcon = React.ComponentPropsWithoutRef + & VariantProps & { + className?: string | undefined + as?: React.ElementType + height?: number + width?: number + } + +const ButtonIcon = React.forwardRef< + React.ElementRef, + IButtonIcon +>(({ className, size, ...props }, ref) => { + const { + variant: parentVariant, + size: parentSize, + action: parentAction, + } = useStyleContext(SCOPE) + + if (typeof size === 'number') { + return ( + + ) + } + else if ( + (props.height !== undefined || props.width !== undefined) + && size === undefined + ) { + return ( + + ) + } + return ( + + ) +}) + +type IButtonGroupProps = React.ComponentPropsWithoutRef + & VariantProps + +const ButtonGroup = React.forwardRef< + React.ElementRef, + IButtonGroupProps +>( + ( + { + className, + space = 'md', + isAttached = false, + flexDirection = 'column', + ...props + }, + ref, + ) => { + return ( + + ) + }, +) + +Button.displayName = 'Button' +ButtonText.displayName = 'ButtonText' +ButtonSpinner.displayName = 'ButtonSpinner' +ButtonIcon.displayName = 'ButtonIcon' +ButtonGroup.displayName = 'ButtonGroup' + +export { Button, ButtonGroup, ButtonIcon, ButtonSpinner, ButtonText } diff --git a/code/app/app-rn/components/ui/gluestack-ui-provider/config.ts b/code/app/app-rn/components/ui/gluestack-ui-provider/config.ts new file mode 100644 index 00000000..dda0adfa --- /dev/null +++ b/code/app/app-rn/components/ui/gluestack-ui-provider/config.ts @@ -0,0 +1,309 @@ +'use client' +import { vars } from 'nativewind' + +export const config = { + light: vars({ + '--color-primary-0': '240 249 255', + '--color-primary-50': '224 242 254', + '--color-primary-100': '186 230 253', + '--color-primary-200': '125 211 252', + '--color-primary-300': '56 189 248', + '--color-primary-400': '14 165 233', + '--color-primary-500': '2 132 199', + '--color-primary-600': '3 105 161', + '--color-primary-700': '7 89 133', + '--color-primary-800': '12 74 110', + '--color-primary-900': '8 47 73', + '--color-primary-950': '2 22 36', + + /* Secondary */ + '--color-secondary-0': '253 253 253', + '--color-secondary-50': '251 251 251', + '--color-secondary-100': '246 246 246', + '--color-secondary-200': '242 242 242', + '--color-secondary-300': '237 237 237', + '--color-secondary-400': '230 230 231', + '--color-secondary-500': '217 217 219', + '--color-secondary-600': '198 199 199', + '--color-secondary-700': '189 189 189', + '--color-secondary-800': '177 177 177', + '--color-secondary-900': '165 164 164', + '--color-secondary-950': '157 157 157', + + /* Tertiary */ + '--color-tertiary-0': '255 250 245', + '--color-tertiary-50': '255 242 229', + '--color-tertiary-100': '255 233 213', + '--color-tertiary-200': '254 209 170', + '--color-tertiary-300': '253 180 116', + '--color-tertiary-400': '251 157 75', + '--color-tertiary-500': '231 129 40', + '--color-tertiary-600': '215 117 31', + '--color-tertiary-700': '180 98 26', + '--color-tertiary-800': '130 73 23', + '--color-tertiary-900': '108 61 19', + '--color-tertiary-950': '84 49 18', + + /* Error */ + '--color-error-0': '254 233 233', + '--color-error-50': '254 226 226', + '--color-error-100': '254 202 202', + '--color-error-200': '252 165 165', + '--color-error-300': '248 113 113', + '--color-error-400': '239 68 68', + '--color-error-500': '230 53 53', + '--color-error-600': '220 38 38', + '--color-error-700': '185 28 28', + '--color-error-800': '153 27 27', + '--color-error-900': '127 29 29', + '--color-error-950': '83 19 19', + + /* Success */ + '--color-success-0': '228 255 244', + '--color-success-50': '202 255 232', + '--color-success-100': '162 241 192', + '--color-success-200': '132 211 162', + '--color-success-300': '102 181 132', + '--color-success-400': '72 151 102', + '--color-success-500': '52 131 82', + '--color-success-600': '42 121 72', + '--color-success-700': '32 111 62', + '--color-success-800': '22 101 52', + '--color-success-900': '20 83 45', + '--color-success-950': '27 50 36', + + /* Warning */ + '--color-warning-0': '255 249 245', + '--color-warning-50': '255 244 236', + '--color-warning-100': '255 231 213', + '--color-warning-200': '254 205 170', + '--color-warning-300': '253 173 116', + '--color-warning-400': '251 149 75', + '--color-warning-500': '231 120 40', + '--color-warning-600': '215 108 31', + '--color-warning-700': '180 90 26', + '--color-warning-800': '130 68 23', + '--color-warning-900': '108 56 19', + '--color-warning-950': '84 45 18', + + /* Info */ + '--color-info-0': '236 248 254', + '--color-info-50': '199 235 252', + '--color-info-100': '162 221 250', + '--color-info-200': '124 207 248', + '--color-info-300': '87 194 246', + '--color-info-400': '50 180 244', + '--color-info-500': '13 166 242', + '--color-info-600': '11 141 205', + '--color-info-700': '9 115 168', + '--color-info-800': '7 90 131', + '--color-info-900': '5 64 93', + '--color-info-950': '3 38 56', + + /* Typography */ + '--color-typography-0': '254 254 255', + '--color-typography-50': '245 245 245', + '--color-typography-100': '229 229 229', + '--color-typography-200': '219 219 220', + '--color-typography-300': '212 212 212', + '--color-typography-400': '163 163 163', + '--color-typography-500': '140 140 140', + '--color-typography-600': '115 115 115', + '--color-typography-700': '82 82 82', + '--color-typography-800': '64 64 64', + '--color-typography-900': '38 38 39', + '--color-typography-950': '23 23 23', + + /* Outline */ + '--color-outline-0': '253 254 254', + '--color-outline-50': '243 243 243', + '--color-outline-100': '230 230 230', + '--color-outline-200': '221 220 219', + '--color-outline-300': '211 211 211', + '--color-outline-400': '165 163 163', + '--color-outline-500': '140 141 141', + '--color-outline-600': '115 116 116', + '--color-outline-700': '83 82 82', + '--color-outline-800': '65 65 65', + '--color-outline-900': '39 38 36', + '--color-outline-950': '26 23 23', + + /* Background */ + '--color-background-0': '255 255 255', + '--color-background-50': '246 246 246', + '--color-background-100': '242 241 241', + '--color-background-200': '220 219 219', + '--color-background-300': '213 212 212', + '--color-background-400': '162 163 163', + '--color-background-500': '142 142 142', + '--color-background-600': '116 116 116', + '--color-background-700': '83 82 82', + '--color-background-800': '65 64 64', + '--color-background-900': '39 38 37', + '--color-background-950': '18 18 18', + + /* Background Special */ + '--color-background-error': '254 241 241', + '--color-background-warning': '255 243 234', + '--color-background-success': '237 252 242', + '--color-background-muted': '247 248 247', + '--color-background-info': '235 248 254', + + /* Focus Ring Indicator */ + '--color-indicator-primary': '55 55 55', + '--color-indicator-info': '83 153 236', + '--color-indicator-error': '185 28 28', + }), + dark: vars({ + '--color-primary-0': '2 22 36', + '--color-primary-50': '8 47 73', + '--color-primary-100': '12 74 110', + '--color-primary-200': '7 89 133', + '--color-primary-300': '3 105 161', + '--color-primary-400': '2 132 199', + '--color-primary-500': '56 189 248', + '--color-primary-600': '125 211 252', + '--color-primary-700': '186 230 253', + '--color-primary-800': '224 242 254', + '--color-primary-900': '240 249 255', + '--color-primary-950': '248 252 255', + + /* Secondary */ + '--color-secondary-0': '20 20 20', + '--color-secondary-50': '23 23 23', + '--color-secondary-100': '31 31 31', + '--color-secondary-200': '39 39 39', + '--color-secondary-300': '44 44 44', + '--color-secondary-400': '56 57 57', + '--color-secondary-500': '63 64 64', + '--color-secondary-600': '86 86 86', + '--color-secondary-700': '110 110 110', + '--color-secondary-800': '135 135 135', + '--color-secondary-900': '150 150 150', + '--color-secondary-950': '164 164 164', + + /* Tertiary */ + '--color-tertiary-0': '84 49 18', + '--color-tertiary-50': '108 61 19', + '--color-tertiary-100': '130 73 23', + '--color-tertiary-200': '180 98 26', + '--color-tertiary-300': '215 117 31', + '--color-tertiary-400': '231 129 40', + '--color-tertiary-500': '251 157 75', + '--color-tertiary-600': '253 180 116', + '--color-tertiary-700': '254 209 170', + '--color-tertiary-800': '255 233 213', + '--color-tertiary-900': '255 242 229', + '--color-tertiary-950': '255 250 245', + + /* Error */ + '--color-error-0': '83 19 19', + '--color-error-50': '127 29 29', + '--color-error-100': '153 27 27', + '--color-error-200': '185 28 28', + '--color-error-300': '220 38 38', + '--color-error-400': '230 53 53', + '--color-error-500': '239 68 68', + '--color-error-600': '249 97 96', + '--color-error-700': '229 91 90', + '--color-error-800': '254 202 202', + '--color-error-900': '254 226 226', + '--color-error-950': '254 233 233', + + /* Success */ + '--color-success-0': '27 50 36', + '--color-success-50': '20 83 45', + '--color-success-100': '22 101 52', + '--color-success-200': '32 111 62', + '--color-success-300': '42 121 72', + '--color-success-400': '52 131 82', + '--color-success-500': '72 151 102', + '--color-success-600': '102 181 132', + '--color-success-700': '132 211 162', + '--color-success-800': '162 241 192', + '--color-success-900': '202 255 232', + '--color-success-950': '228 255 244', + + /* Warning */ + '--color-warning-0': '84 45 18', + '--color-warning-50': '108 56 19', + '--color-warning-100': '130 68 23', + '--color-warning-200': '180 90 26', + '--color-warning-300': '215 108 31', + '--color-warning-400': '231 120 40', + '--color-warning-500': '251 149 75', + '--color-warning-600': '253 173 116', + '--color-warning-700': '254 205 170', + '--color-warning-800': '255 231 213', + '--color-warning-900': '255 244 237', + '--color-warning-950': '255 249 245', + + /* Info */ + '--color-info-0': '3 38 56', + '--color-info-50': '5 64 93', + '--color-info-100': '7 90 131', + '--color-info-200': '9 115 168', + '--color-info-300': '11 141 205', + '--color-info-400': '13 166 242', + '--color-info-500': '50 180 244', + '--color-info-600': '87 194 246', + '--color-info-700': '124 207 248', + '--color-info-800': '162 221 250', + '--color-info-900': '199 235 252', + '--color-info-950': '236 248 254', + + /* Typography */ + '--color-typography-0': '23 23 23', + '--color-typography-50': '38 38 39', + '--color-typography-100': '64 64 64', + '--color-typography-200': '82 82 82', + '--color-typography-300': '115 115 115', + '--color-typography-400': '140 140 140', + '--color-typography-500': '163 163 163', + '--color-typography-600': '212 212 212', + '--color-typography-700': '219 219 220', + '--color-typography-800': '229 229 229', + '--color-typography-900': '245 245 245', + '--color-typography-950': '254 254 255', + + /* Outline */ + '--color-outline-0': '26 23 23', + '--color-outline-50': '39 38 36', + '--color-outline-100': '65 65 65', + '--color-outline-200': '83 82 82', + '--color-outline-300': '115 116 116', + '--color-outline-400': '140 141 141', + '--color-outline-500': '165 163 163', + '--color-outline-600': '211 211 211', + '--color-outline-700': '221 220 219', + '--color-outline-800': '230 230 230', + '--color-outline-900': '243 243 243', + '--color-outline-950': '253 254 254', + + /* Background */ + '--color-background-0': '18 18 18', + '--color-background-50': '39 38 37', + '--color-background-100': '65 64 64', + '--color-background-200': '83 82 82', + '--color-background-300': '116 116 116', + '--color-background-400': '142 142 142', + '--color-background-500': '162 163 163', + '--color-background-600': '213 212 212', + '--color-background-700': '229 228 228', + '--color-background-800': '242 241 241', + '--color-background-900': '246 246 246', + '--color-background-950': '255 255 255', + + /* Background Special */ + '--color-background-error': '66 43 43', + '--color-background-warning': '65 47 35', + '--color-background-success': '28 43 33', + '--color-background-muted': '51 51 51', + '--color-background-info': '26 40 46', + + /* Focus Ring Indicator */ + '--color-indicator-primary': '247 247 247', + '--color-indicator-info': '161 199 245', + '--color-indicator-error': '232 70 69', + }), +} diff --git a/code/app/app-rn/components/ui/gluestack-ui-provider/dependencies.json b/code/app/app-rn/components/ui/gluestack-ui-provider/dependencies.json new file mode 100644 index 00000000..393c7fb2 --- /dev/null +++ b/code/app/app-rn/components/ui/gluestack-ui-provider/dependencies.json @@ -0,0 +1,5 @@ +{ + "dependencies": {}, + "devDependencies": {}, + "components": [] +} diff --git a/code/app/app-rn/components/ui/gluestack-ui-provider/index.next15.tsx b/code/app/app-rn/components/ui/gluestack-ui-provider/index.next15.tsx new file mode 100644 index 00000000..4fafc408 --- /dev/null +++ b/code/app/app-rn/components/ui/gluestack-ui-provider/index.next15.tsx @@ -0,0 +1,87 @@ +// This is a Next.js 15 compatible version of the GluestackUIProvider +'use client'; +import React, { useEffect, useLayoutEffect } from 'react'; +import { config } from './config'; +import { OverlayProvider } from '@gluestack-ui/core/overlay/creator'; +import { ToastProvider } from '@gluestack-ui/core/toast/creator'; +import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils'; +import { script } from './script'; + +const variableStyleTagId = 'nativewind-style'; +const createStyle = (styleTagId: string) => { + const style = document.createElement('style'); + style.id = styleTagId; + style.appendChild(document.createTextNode('')); + return style; +}; + +export const useSafeLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +export function GluestackUIProvider({ + mode = 'light', + ...props +}: { + mode?: 'light' | 'dark' | 'system'; + children?: React.ReactNode; +}) { + let cssVariablesWithMode = ``; + Object.keys(config).forEach((configKey) => { + cssVariablesWithMode += + configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`; + const cssVariables = Object.keys( + config[configKey as keyof typeof config] + ).reduce((acc: string, curr: string) => { + acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `; + return acc; + }, ''); + cssVariablesWithMode += `${cssVariables} \n}`; + }); + + setFlushStyles(cssVariablesWithMode); + + const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => { + script(e.matches ? 'dark' : 'light'); + }, []); + + useSafeLayoutEffect(() => { + if (mode !== 'system') { + const documentElement = document.documentElement; + if (documentElement) { + documentElement.classList.add(mode); + documentElement.classList.remove(mode === 'light' ? 'dark' : 'light'); + documentElement.style.colorScheme = mode; + } + } + }, [mode]); + + useSafeLayoutEffect(() => { + if (mode !== 'system') return; + const media = window.matchMedia('(prefers-color-scheme: dark)'); + + media.addListener(handleMediaQuery); + + return () => media.removeListener(handleMediaQuery); + }, [handleMediaQuery]); + + useSafeLayoutEffect(() => { + if (typeof window !== 'undefined') { + const documentElement = document.documentElement; + if (documentElement) { + const head = documentElement.querySelector('head'); + let style = head?.querySelector(`[id='${variableStyleTagId}']`); + if (!style) { + style = createStyle(variableStyleTagId); + style.innerHTML = cssVariablesWithMode; + if (head) head.appendChild(style); + } + } + } + }, []); + + return ( + + {props.children} + + ); +} diff --git a/code/app/app-rn/components/ui/gluestack-ui-provider/index.tsx b/code/app/app-rn/components/ui/gluestack-ui-provider/index.tsx new file mode 100644 index 00000000..b671d5b3 --- /dev/null +++ b/code/app/app-rn/components/ui/gluestack-ui-provider/index.tsx @@ -0,0 +1,40 @@ +import type { ViewProps } from 'react-native' +import { OverlayProvider } from '@gluestack-ui/core/overlay/creator' +import { ToastProvider } from '@gluestack-ui/core/toast/creator' +import { useColorScheme } from 'nativewind' +import * as React from 'react' +import { useEffect } from 'react' +import { View } from 'react-native' +import { config } from './config' + +export type ModeType = 'light' | 'dark' | 'system' + +export function GluestackUIProvider({ + mode = 'light', + ...props +}: { + mode?: ModeType + children?: React.ReactNode + style?: ViewProps['style'] +}) { + const { colorScheme, setColorScheme } = useColorScheme() + + useEffect(() => { + setColorScheme(mode) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mode]) + + return ( + + + {props.children} + + + ) +} diff --git a/code/app/app-rn/components/ui/gluestack-ui-provider/index.web.tsx b/code/app/app-rn/components/ui/gluestack-ui-provider/index.web.tsx new file mode 100644 index 00000000..610b6ad6 --- /dev/null +++ b/code/app/app-rn/components/ui/gluestack-ui-provider/index.web.tsx @@ -0,0 +1,96 @@ +'use client'; +import React, { useEffect, useLayoutEffect } from 'react'; +import { config } from './config'; +import { OverlayProvider } from '@gluestack-ui/core/overlay/creator'; +import { ToastProvider } from '@gluestack-ui/core/toast/creator'; +import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils'; +import { script } from './script'; + +export type ModeType = 'light' | 'dark' | 'system'; + +const variableStyleTagId = 'nativewind-style'; +const createStyle = (styleTagId: string) => { + const style = document.createElement('style'); + style.id = styleTagId; + style.appendChild(document.createTextNode('')); + return style; +}; + +export const useSafeLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +export function GluestackUIProvider({ + mode = 'light', + ...props +}: { + mode?: ModeType; + children?: React.ReactNode; +}) { + let cssVariablesWithMode = ``; + Object.keys(config).forEach((configKey) => { + cssVariablesWithMode += + configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`; + const cssVariables = Object.keys( + config[configKey as keyof typeof config] + ).reduce((acc: string, curr: string) => { + acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `; + return acc; + }, ''); + cssVariablesWithMode += `${cssVariables} \n}`; + }); + + setFlushStyles(cssVariablesWithMode); + + const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => { + script(e.matches ? 'dark' : 'light'); + }, []); + + useSafeLayoutEffect(() => { + if (mode !== 'system') { + const documentElement = document.documentElement; + if (documentElement) { + documentElement.classList.add(mode); + documentElement.classList.remove(mode === 'light' ? 'dark' : 'light'); + documentElement.style.colorScheme = mode; + } + } + }, [mode]); + + useSafeLayoutEffect(() => { + if (mode !== 'system') return; + const media = window.matchMedia('(prefers-color-scheme: dark)'); + + media.addListener(handleMediaQuery); + + return () => media.removeListener(handleMediaQuery); + }, [handleMediaQuery]); + + useSafeLayoutEffect(() => { + if (typeof window !== 'undefined') { + const documentElement = document.documentElement; + if (documentElement) { + const head = documentElement.querySelector('head'); + let style = head?.querySelector(`[id='${variableStyleTagId}']`); + if (!style) { + style = createStyle(variableStyleTagId); + style.innerHTML = cssVariablesWithMode; + if (head) head.appendChild(style); + } + } + } + }, []); + + return ( + <> +