diff --git a/.env.dev b/.env.dev index d227497..8f637fc 100644 --- a/.env.dev +++ b/.env.dev @@ -1,5 +1,13 @@ URLPATH = uploads PORT = 3101 +LINE_ACCESS_TOKEN = lbXrLMGoiOvBTG3vFNlVNQbolcVkQzqi920DVwrbtr1 prikeyPath = ./certificate/RSA-privkey.pem certPath = ./certificate/RSA-cert.pem -cafilePath = ./certificate/RSA-chain.pem \ No newline at end of file +cafilePath = ./certificate/RSA-chain.pem + +# DB---------------------------------------------- +DB_HOST = 192.168.0.15 +DB_PORT = 3307 +DB_USER = jianmiau +DB_PASSWORD = VQ*ZetC7xcc9%dTW +DB_DATABASE = badminton \ No newline at end of file diff --git a/.env.prod b/.env.prod index 39d924f..26cb11b 100644 --- a/.env.prod +++ b/.env.prod @@ -1,5 +1,13 @@ URLPATH = uploads PORT = 3100 +LINE_ACCESS_TOKEN = lbXrLMGoiOvBTG3vFNlVNQbolcVkQzqi920DVwrbtr1 prikeyPath = /certificate/RSA-privkey.pem certPath = /certificate/RSA-cert.pem -cafilePath = /certificate/RSA-chain.pem \ No newline at end of file +cafilePath = /certificate/RSA-chain.pem + +# DB---------------------------------------------- +DB_HOST = 192.168.0.15 +DB_PORT = 3307 +DB_USER = jianmiau +DB_PASSWORD = VQ*ZetC7xcc9%dTW +DB_DATABASE = badminton \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0370624 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // 使用 IntelliSense 以得知可用的屬性。 + // 暫留以檢視現有屬性的描述。 + // 如需詳細資訊,請瀏覽: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Attach to Remote", + "address": "jianmiau.tk:9229/87f42d5b-97bf-4d25-a4d7-37306985459a", + "port": 9229, + "localRoot": "${workspaceFolder}", + // "remoteRoot": "W:/home/www/api", + "remoteRoot": "/volume1/homes/JianMiau/www/api", + "skipFiles": [ + "/**" + ] + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 0af0683..7bbc887 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,19 @@ -# sudo docker build -t linebotts . - -# 将 Docker 镜像保存为文件 -# sudo docker save -o linebotts.tar linebotts - -# 使用 scp 传输文件到目标机器 -# scp -P 20022 linebotts.tar jianmiau@192.168.0.77:~/Nas/docker - -# 在目标机器上加载镜像 -# sudo docker load -i /path/to/destination/linebotts.tar - - - -# sudo docker run -v /volume1/homes/JianMiau/www/certificate:/certificate -e TZ=Asia/Taipei --name=linebotts --restart always --net=host linebotts +# sudo docker build -t api . # 後續查看容器 # docker ps # sudo docker exec -it [Container ID] /bin/bash # 選擇node -FROM node:19.4.0 +FROM node:20.9.0 + +# 安装系统依赖 +RUN apt-get update && apt-get install -y \ + libcairo2-dev \ + libjpeg-dev \ + libpango1.0-dev \ + libgif-dev \ + build-essential # 指定NODE_ENV為production ENV NODE_ENV=production @@ -28,8 +23,8 @@ WORKDIR /app VOLUME ["/certificate"] -# 只copy package.json檔案 -COPY ["package.json", "./"] +# 复制 package.json +COPY package.json ./ # 安裝dependencies # If you are building your code for production @@ -40,4 +35,19 @@ RUN npm install COPY . . # 指定啟動container後執行命令 -CMD [ "npm", "start" ] \ No newline at end of file +CMD [ "npm", "start" ] + + + +# 将 Docker 镜像保存为文件 +# sudo docker save -o api.tar api + +# 使用 scp 传输文件到目标机器 +# scp -P 20022 api.tar jianmiau@192.168.0.77:~/Nas/docker + +# 在目标机器上加载镜像 +# sudo docker load -i /path/to/destination/api.tar + + + +# sudo docker run -v /volume1/homes/JianMiau/www/certificate:/certificate -e TZ=Asia/Taipei --name=api --restart always --net=host api \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b4f2b5b..b439a35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,26 @@ { - "name": "canvas", + "name": "api", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "canvas", + "name": "api", "version": "1.0.0", "license": "ISC", "dependencies": { + "axios": "^1.7.7", "canvas": "^2.11.2", "dateformat": "^4.5.1", "dayjs": "^1.11.7", "dotenv": "^16.0.3", "express": "^4.20.0", + "form-data": "^4.0.0", "fs": "^0.0.1-security", "multer": "^1.4.5-lts.1", - "nodemon": "^3.1.4" + "mysql": "^2.18.1", + "nodemon": "^3.1.4", + "ts-node": "^10.9.2" }, "devDependencies": { "@types/dateformat": "^5.0.0", @@ -25,6 +29,39 @@ "@types/node": "^22.5.4" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -44,6 +81,26 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -118,7 +175,6 @@ "version": "22.5.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", - "dev": true, "dependencies": { "undici-types": "~6.19.2" } @@ -173,6 +229,28 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -227,16 +305,44 @@ "node": ">=10" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -413,6 +519,17 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -501,6 +618,11 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -557,6 +679,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -587,6 +717,14 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -751,6 +889,38 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1096,6 +1266,11 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1255,6 +1430,55 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/mysql/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/mysql/node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/nan": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", @@ -1435,6 +1659,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -1847,6 +2076,48 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1864,6 +2135,19 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1872,8 +2156,7 @@ "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unpipe": { "version": "1.0.0", @@ -1896,6 +2179,11 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1943,6 +2231,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } } } } diff --git a/package.json b/package.json index 3977a43..adbef9c 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,18 @@ "license": "ISC", "keywords": [], "dependencies": { + "axios": "^1.7.7", "canvas": "^2.11.2", "dateformat": "^4.5.1", "dayjs": "^1.11.7", "dotenv": "^16.0.3", "express": "^4.20.0", + "form-data": "^4.0.0", "fs": "^0.0.1-security", "multer": "^1.4.5-lts.1", - "nodemon": "^3.1.4" + "mysql": "^2.18.1", + "nodemon": "^3.1.4", + "ts-node": "^10.9.2" }, "devDependencies": { "@types/dateformat": "^5.0.0", @@ -28,4 +32,4 @@ "@types/multer": "^1.4.12", "@types/node": "^22.5.4" } -} \ No newline at end of file +} diff --git a/src/Engine/CCExtensions/ArrayExtension.ts b/src/Engine/CCExtensions/ArrayExtension.ts new file mode 100644 index 0000000..0dab41d --- /dev/null +++ b/src/Engine/CCExtensions/ArrayExtension.ts @@ -0,0 +1,116 @@ +declare interface Array { + /** + * 移除一個值並且回傳 + * @param index + */ + ExRemoveAt(index: number): T; + /** + * 移除全部值(注意. 參考的也會被清空) + * @example + * + * let bar: number[] = [1, 2, 3]; + * let bar2: number[] = bar; + * bar.Clear(); + * console.log(bar, bar2); + * + * // { + * // "bar": [], + * // "bar2": [] + * // } + */ + Clear(): void; + /** + * 物件陣列排序,asc&key陣列長度請一樣 + * PS. boolean 帶false是先true在false + * @link JavaScript Object 排序 http://www.eion.com.tw/Blogger/?Pid=1170#:~:text=JavaScript%20Object%20排序 + * @param asc 是否升序排列(小到大) + * @param key 需排序的key(優先順序左到右)(沒有就放空) + */ + ObjectSort(asc?: boolean[], key?: string[]): any[]; + /** + * 設計給ArrayforHoldButton使用 + * Add a non persistent listener to the UnityEvent. + * @param call Callback function. + */ + AddListener(call: Function): void; +} + +Array.prototype.ExRemoveAt || Object.defineProperty(Array.prototype, "ExRemoveAt", { + enumerable: false, + value: function (index: number): any { + let item: any = this.splice(index, 1); + return item[0]; + } +}); + +Array.prototype.Clear || Object.defineProperty(Array.prototype, "Clear", { + enumerable: false, + value: function (): void { + this.length = 0; + + // let foo: number[] = [1, 2, 3]; + // let bar: number[] = [1, 2, 3]; + // let foo2: number[] = foo; + // let bar2: number[] = bar; + // foo = []; + // bar.length = 0; + // console.log(foo, bar, foo2, bar2); + + // { + // "foo": [], + // "bar": [], + // "foo2": [ + // 1, + // 2, + // 3 + // ], + // "bar2": [] + // } + } +}); + +Array.prototype.ObjectSort || Object.defineProperty(Array.prototype, "ObjectSort", { + enumerable: false, + /** + * @param asc 是否升序排列(小到大) + * @param key 需排序的key(優先順序左到右)(沒有就放空) + */ + value: function (asc: boolean[] = [true], key?: string[]): any[] { + if (this.length === 0) { + return this; + } else if (!key || key.length === 0) { + console.error(`ObjectSort key error`); + return this; + } else if (asc.length !== key.length) { + console.error(`ObjectSort key asc error asc.length: ${asc.length}, key.length: ${key.length}`); + return this; + } + for (let i: number = 0; i < key.length; i++) { + const keyname: string = key[i]; + if (this[0][keyname] === undefined) { + console.error(`ObjectSort has not key[${i}]: ${keyname}`); + return this; + } + } + let count: number = key ? key.length : 1; + let arr: any[]; + for (let i: number = count - 1; i >= 0; i--) { + arr = this.sort(function (a: any, b: any): 1 | -1 { + let mya: any = a; + let myb: any = b; + if (key) { + mya = a[key[i]]; + myb = b[key[i]]; + } + + // 加個等於數字相同不要再去排序到 + if (asc[i]) { + return mya >= myb ? 1 : -1; + } else { + return mya <= myb ? 1 : -1; + } + }); + } + return arr; + } +}); \ No newline at end of file diff --git a/src/Engine/CCExtensions/CCExtension.ts.meta b/src/Engine/CCExtensions/CCExtension.ts.meta new file mode 100644 index 0000000..1ab1865 --- /dev/null +++ b/src/Engine/CCExtensions/CCExtension.ts.meta @@ -0,0 +1,10 @@ +{ + "ver": "1.1.0", + "uuid": "b373f805-9297-4af5-8ea6-0a250649b5b0", + "importer": "typescript", + "isPlugin": false, + "loadPluginInWeb": true, + "loadPluginInNative": true, + "loadPluginInEditor": false, + "subMetas": {} +} \ No newline at end of file diff --git a/src/Engine/CCExtensions/NumberExtension.ts b/src/Engine/CCExtensions/NumberExtension.ts new file mode 100644 index 0000000..5d39881 --- /dev/null +++ b/src/Engine/CCExtensions/NumberExtension.ts @@ -0,0 +1,189 @@ + +declare interface Number { + + /** + * 金額每三位數(千)加逗號, 並且補到小數點第2位 + * 輸出 41,038,560.00 + * @param precision 補到小數點第幾位 + * @param isPadZero 是否要補零 + * */ + ExFormatNumberWithComma(precision?: number, isPadZero?: boolean): string; + /** + * 基本4位數(9,999-999B-T) + * */ + ExTransferToBMK(precision?: number,offset?: number): string; + /** + * 數字轉字串, 頭補0 + * @param size + */ + Pad(size: number): string; + /** + * 四捨五入到小數點第X位 (同server計算規則) + * @param precision + */ + ExToNumRoundDecimal(precision: number): number; + /** + * 無條件捨去到小數點第X位 + * @param precision + */ + ExToNumFloorDecimal(precision: number): number; + /** + * 無條件捨去強制保留X位小數,如:2,會在2後面補上00.即2.00 + * @param precision 補到小數點第幾位 + * @param isPadZero 是否要補零 + */ + ExToStringFloorDecimal(precision: number, isPadZero?: boolean): string; + /** + * 取整數) + */ + ExToInt():number; + /** + * 小數轉整數(支援科學符號) + */ + Float2Fixed():number; + /** + * 數字長度(支援科學符號) + */ + DigitLength():number; + + target: number; + + +} + +Number.prototype.ExFormatNumberWithComma || Object.defineProperty(Number.prototype, 'ExFormatNumberWithComma', { + enumerable: false, + value: function (precision: number = 2, isPadZero: boolean = true) { + + // let arr = String(this).split('.'); + let arr = this.ExToStringFloorDecimal(precision, isPadZero).split('.'); + let num = arr[0], result = ''; + while (num.length > 3) { + result = ',' + num.slice(-3) + result; + num = num.slice(0, num.length - 3); + } + if (num.length > 0) result = num + result; + return arr[1] ? result + '.' + arr[1] : result; + } +}) + + +Number.prototype.ExTransferToBMK || Object.defineProperty(Number.prototype, 'ExTransferToBMK', { + enumerable: false, + value: function (precision: number=2,offset: number = 0) { + /**千 */ + let MONEY_1K: number = 1000; + /**萬 */ + // let MONEY_10K: number = 10000; + /**十萬 */ + // let MONEY_100K: number = 100000; + /**百萬 */ + let MONEY_1M: number = 1000000; + /**千萬 */ + // let MONEY_10M: number = 10000000; + /**億 */ + // let MONEY_100M: number = 100000000; + /**十億 */ + let MONEY_1B: number = 1000000000; + /**百億 */ + // let MONEY_10B: number = 10000000000; + /**千億 */ + // let MONEY_100B: number = 100000000000; + /**兆 */ + // let MONEY_1T: number = 1000000000000; + offset = Math.pow(10, offset); + // if (this >= MONEY_1T * offset) { + // //(3)1,000T + // //1T~ + // return (~~(this / MONEY_1T)).ExFormatNumberWithComma(0) + "T"; + // } + if (this >= MONEY_1B * offset) { + //1,000B~900,000B + //1B~900B + return (this / MONEY_1B).ExFormatNumberWithComma(3, false) + "B"; + } + else if (this >= MONEY_1M * offset) { + //1,000M~900,000M + //1M~900M + return (this / MONEY_1M).ExFormatNumberWithComma(3, false) + "M"; + } + else if (this >= MONEY_1K * offset) { + //1,000K~900,000K + //1K~90K + return (this / MONEY_1K).ExFormatNumberWithComma(3, false) + "K"; + } + else { + //0~9,000,000 + //0~9,000 + return this.ExFormatNumberWithComma(precision); + } + } +}) +Number.prototype.Pad || Object.defineProperty(Number.prototype, 'Pad', { + enumerable: false, + value: function (size: number) { + let s = this + ""; + while (s.length < size) s = "0" + s; + return s; + } +}) +Number.prototype.ExToNumRoundDecimal || Object.defineProperty(Number.prototype, 'ExToNumRoundDecimal', { + enumerable: false, + value: function (precision: number) { + return Math.round(Math.round(this * Math.pow(10, (precision || 0) + 1)) / 10) / Math.pow(10, (precision || 0)); + } +}) +Number.prototype.ExToInt || Object.defineProperty(Number.prototype, 'ExToInt',{ + enumerable: false, + value: function (){ + return ~~this; + } +}) +Number.prototype.ExToNumFloorDecimal || Object.defineProperty(Number.prototype, 'ExToNumFloorDecimal', { + enumerable: false, + value: function (precision: number) { + let str = this.toPrecision(12); + let dotPos = str.indexOf('.'); + return dotPos == -1 ? this : +`${str.substr(0, dotPos + 1 + precision)}`; + } +}) +Number.prototype.ExToStringFloorDecimal || Object.defineProperty(Number.prototype, 'ExToStringFloorDecimal', { + enumerable: false, + value: function (precision: number, isPadZero: boolean = true) { + // 取小數點第X位 + let f = this.ExToNumFloorDecimal(precision); + let s = f.toString(); + // 補0 + if (isPadZero) { + let rs = s.indexOf('.'); + if (rs < 0) { + rs = s.length; + s += '.'; + } + while (s.length <= rs + precision) { + s += '0'; + } + } + return s; + } +}) +Number.prototype.Float2Fixed || Object.defineProperty(Number.prototype, 'Float2Fixed', { + enumerable: false, + value: function () { + if (this.toString().indexOf('e') === -1) { + return Number(this.toString().replace('.', '')); + } + const dLen = this.DigitLength(); + return dLen > 0 ? +parseFloat((this * Math.pow(10, dLen)).toPrecision(12)) : this; + } +}) +Number.prototype.DigitLength || Object.defineProperty(Number.prototype, 'DigitLength', { + enumerable: false, + value: function () { + const eSplit = this.toString().split(/[eE]/); + const len = (eSplit[0].split('.')[1] || '').length - (+(eSplit[1] || 0)); + return len > 0 ? len : 0; + } +}) + + \ No newline at end of file diff --git a/src/Engine/Number/NumberEx.ts b/src/Engine/Number/NumberEx.ts new file mode 100644 index 0000000..b9ad5e1 --- /dev/null +++ b/src/Engine/Number/NumberEx.ts @@ -0,0 +1,84 @@ +export module NumberEx { + + /** + * 检测数字是否越界,如果越界给出提示 + * @param {*number} num 输入数 + */ + function checkBoundary(num: number) { + if (_boundaryCheckingState) { + if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) { + console.warn(`${num} is beyond boundary when transfer to integer, the results may not be accurate`); + } + } + } + + /** + * 精确乘法 + */ + export function times(num1: number, num2: number, ...others: number[]): number { + if (others.length > 0) { + return times(times(num1, num2), others[0], ...others.slice(1)); + } + const num1Changed = num1.Float2Fixed(); + const num2Changed = num2.Float2Fixed(); + const baseNum = num1.DigitLength() + num2.DigitLength(); + const leftValue = num1Changed * num2Changed; + + checkBoundary(leftValue); + + return leftValue / Math.pow(10, baseNum); + } + + /** + * 精确加法 + */ + export function plus(num1: number, num2: number, ...others: number[]): number { + if (others.length > 0) { + return plus(plus(num1, num2), others[0], ...others.slice(1)); + } + const baseNum = Math.pow(10, Math.max(num1.DigitLength(), num2.DigitLength())); + return (times(num1, baseNum) + times(num2, baseNum)) / baseNum; + } + + /** + * 精确减法 + */ + export function minus(num1: number, num2: number, ...others: number[]): number { + if (others.length > 0) { + return minus(minus(num1, num2), others[0], ...others.slice(1)); + } + const baseNum = Math.pow(10, Math.max(num1.DigitLength(), num2.DigitLength())); + return (times(num1, baseNum) - times(num2, baseNum)) / baseNum; + } + + /** + * 精确除法 + */ + export function divide(num1: number, num2: number, ...others: number[]): number { + if (others.length > 0) { + return divide(divide(num1, num2), others[0], ...others.slice(1)); + } + const num1Changed = num1.Float2Fixed(); + const num2Changed = num2.Float2Fixed(); + checkBoundary(num1Changed); + checkBoundary(num2Changed); + return times((num1Changed / num2Changed), Math.pow(10, num2.DigitLength() - num1.DigitLength())); + } + + /** + * 四舍五入 + */ + export function round(num: number, ratio: number): number { + const base = Math.pow(10, ratio); + return divide(Math.round(times(num, base)), base); + } + + let _boundaryCheckingState = false; + /** + * 是否进行边界检查 + * @param flag 标记开关,true 为开启,false 为关闭 + */ + function enableBoundaryChecking(flag = true) { + _boundaryCheckingState = flag; + } +} \ No newline at end of file diff --git a/src/Engine/Number/RandomEx.ts b/src/Engine/Number/RandomEx.ts new file mode 100644 index 0000000..1bfc080 --- /dev/null +++ b/src/Engine/Number/RandomEx.ts @@ -0,0 +1,90 @@ + +export module RandomEx { + + /** + * 取得隨機布林值 + */ + export function GetBool() { + return GetInt() >= 0; + } + /** + * 取得隨機整數(回傳min ~ max - 1) + * @param min + * @param max + */ + export function GetInt(min: number = Number.MIN_VALUE, max: number = Number.MAX_VALUE): number { + return Math.floor(Math.random() * (max - min)) + min; + } + /** + * 取得隨機小數 + * @param min + * @param max + */ + export function GetFloat(min: number = Number.MIN_VALUE, max: number = Number.MAX_VALUE): number { + return Math.random() * (max - min) + min; + } + /** + * 隨機取得複數個不重複回傳 + * @param num 取得數量 + * @param items 陣列 + */ + export function GetMultiNoRepeat(num: number, items: any[]): any[] { + let result: any[] = []; + for (let i: number = 0; i < num; i++) { + let ran: number = Math.floor(Math.random() * items.length); + let item = items.splice(ran, 1)[0]; + if (result.indexOf(item) == -1) { + result.push(item); + } + }; + return result; + } + + /** + * 根據權重取得複數個不重複回傳 + * @param prize 獎項 + * @param weights 機率 + * @param count 數量 + */ + export function GetMultiNoRepeatByWeight(prize: any[], weights: number[] = null, count: number = 1): any[] { + if (weights === null) { + weights = []; + for (let i: number = 0; i < prize.length; i++) { + weights.push(1); + } + } + let target: any[] = []; + for (let i: number = 0; i < count; i++) { + let results: number[] = RandomEx.GetPrizeByWeight(prize, weights); + prize.splice(results[0], 1); + weights.splice(results[0], 1); + target.push(results[1]); + } + return target; + } + + + /** + * 根據權重隨機取值 + * @param prize 獎項 + * @param weights 機率 + */ + export function GetPrizeByWeight(prize: any[], weights: number[]): any[] { + if (prize.length !== weights.length) { + console.error(`GetWeight error -> prize.length:${prize.length} !== weights.length:${weights.length}`); + return null; + } + let totalWeight: number = 0; + for (let i: number = 0; i < weights.length; i++) { + totalWeight += weights[i]; + } + let random: number = RandomEx.GetInt(0, totalWeight) + 1; + let nowWeight: number = weights[0]; + for (let i: number = 0; i < weights.length; i++) { + if (nowWeight >= random) { + return [i, prize[i]]; + } + nowWeight += weights[i + 1]; + } + } +} diff --git a/src/Engine/String.ts b/src/Engine/String.ts new file mode 100644 index 0000000..33b622e --- /dev/null +++ b/src/Engine/String.ts @@ -0,0 +1,16 @@ +interface StringConstructor { + IsNullOrEmpty: (value: string) => boolean; + Format: (format: string, ...args: any[]) => string; +} + +String.IsNullOrEmpty = function (value: string): boolean { + return value === undefined || value === null || value.trim() === ""; +}; + +String.Format = function (format: string, ...args: any[]): string { + return format.replace(/{(\d+)}/g, (match, index) => { + let value: any = args[index]; + if (value === null || value === undefined) { return ""; } + return "" + value; + }); +}; diff --git a/src/Tools.ts b/src/Tools.ts new file mode 100644 index 0000000..ab7e94e --- /dev/null +++ b/src/Tools.ts @@ -0,0 +1,13 @@ +/** + * Tools + */ +export default class Tools { + + //#region Custom + + public static Sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + //#endregion +} diff --git a/src/app.ts b/src/app.ts index 171d905..c084b5a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,12 +6,15 @@ import dayjs from "dayjs"; import "dayjs/locale/zh-tw"; import dotenv from "dotenv"; +import "./Engine/CCExtensions/ArrayExtension"; +import "./Engine/CCExtensions/NumberExtension"; +import "./Engine/String"; +import bodyParser from "body-parser"; import dateFormat from "dateformat"; -import express from 'express'; +import express from "express"; import fs from "fs"; -import https from 'https'; -import multer from 'multer'; +import https from "https"; import { generateImage } from "./canvas"; dayjs.locale("zh-tw"); @@ -24,7 +27,6 @@ if (process.env.NODE_ENV) { const path: string = process.env.URLPATH || "/"; const port: number = +process.env.PORT || 3000; const app = express(); -const upload = multer({ dest: `${path}/` }); // 用于处理文件上传 //讀取憑證及金鑰 const prikey: string = fs.readFileSync(process.env.prikeyPath, "utf8"); @@ -38,11 +40,18 @@ const credentials: Object = { ca: cafile }; -// 处理 POST 请求并返回生成的图片 -app.post(`/${path}`, upload.single('file'), (req, res) => { - const imageBuffer = generateImage(); - res.set('Content-Type', 'image/png'); - res.send(imageBuffer); +app.use(bodyParser.json()); +// 设置 POST 路由来返回图片 +app.post(`/${path}`, (req, res) => { + try { + const data = req.body; + const imageBuffer = generateImage(data); + res.setHeader("Content-Type", "image/png"); + res.send(imageBuffer); + } catch (error) { + console.error("Error generating image:", error); + res.status(500).send("Error generating image"); + } }); // 创建 HTTPS 服务器 diff --git a/src/canvas.ts b/src/canvas.ts index a53f259..8bdddc4 100644 --- a/src/canvas.ts +++ b/src/canvas.ts @@ -1,24 +1,36 @@ -import { createCanvas } from 'canvas'; +import { createCanvas, registerFont } from "canvas"; +import dateFormat from "dateformat"; +import { lineNotify } from "./lineNotify"; +import { sqlSendQuery } from "./sql"; // 生成图片的函数 -export function generateImage() { - const boys = ['男生A', '男生B', '男生C']; - const girls = ['女生A', '女生B', '女生C']; - const rounds = 3; // 可以根据需要设置轮数 - const person = boys.length > girls.length ? boys.length : girls.length; - if (boys.length > girls.length) { - girls.push("那個") - } else if (boys.length < girls.length) { - boys.push("那個") +export function generateImage(props: ICanvas) { + const { date, team, rounds = 3 } = props; + const formatDate = `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`; + const { aTeam, bTeam } = splitTeams(team); + + if (!aTeam || !bTeam) { + return ""; + } + const person = aTeam.length > bTeam.length ? aTeam.length : bTeam.length; + if (aTeam.length > bTeam.length) { + bTeam.push("那個") + } else if (aTeam.length < bTeam.length) { + aTeam.push("那個") } const canvasWidth = 800; // 根据需要调整宽度 const canvasHeight = (rounds * 100) + (person * 10); // 根据需要调整高度 const canvas = createCanvas(canvasWidth, canvasHeight); - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); + + + // 注册自定义字体 + const fontPath = "./src/fonts/PMingLiU.ttf"; + registerFont(fontPath, { family: "PMingLiU" }); const font = "PMingLiU"; // 设定背景颜色 - ctx.fillStyle = 'white'; + ctx.fillStyle = "white"; ctx.fillRect(0, 0, canvas.width, canvas.height); // 追踪已使用的组合 @@ -27,11 +39,11 @@ export function generateImage() { // 生成不重复的队伍组合并确保每人上场 function generateTeams(): { teamA: string, teamB: string }[] { const roundTeams: { teamA: string, teamB: string }[] = []; - const availableBoys = [...boys]; - const availableGirls = [...girls]; + const availableBoys = [...aTeam]; + const availableGirls = [...bTeam]; let attempts = 0; - while (roundTeams.length < boys.length) { + while (roundTeams.length < aTeam.length) { const boyIndex = Math.floor(Math.random() * availableBoys.length); const girlIndex = Math.floor(Math.random() * availableGirls.length); const team = `${availableBoys[boyIndex]}-${availableGirls[girlIndex]}`; @@ -49,7 +61,7 @@ export function generateImage() { // 如果尝试过多次未找到合适组合,强制生成剩余队伍 attempts++; - if (attempts > 10 && roundTeams.length < boys.length) { + if (attempts > 10 && roundTeams.length < aTeam.length) { for (let i = 0; i < availableBoys.length; i++) { roundTeams.push({ teamA: availableBoys[i], @@ -70,43 +82,43 @@ export function generateImage() { teamCombinations.push(newRound); // 如果所有组合都用过了,重置组合记录 - if (usedCombinations.size >= boys.length * girls.length) { + if (usedCombinations.size >= aTeam.length * bTeam.length) { usedCombinations.clear(); } } // 绘制标题和日期 - ctx.fillStyle = 'black'; + ctx.fillStyle = "black"; ctx.font = `bold 24px "${font}"`; - ctx.textAlign = 'center'; - ctx.fillText('比賽隊伍', canvasWidth / 2, 40); + ctx.textAlign = "center"; + ctx.fillText(`${formatDate} 比賽隊伍`, canvasWidth / 2, 40); - ctx.font = `italic 16px "${font}"`; - ctx.fillText('2024-09-09', canvasWidth / 2, 70); - - const roundTitleYStart = 120; + const roundTitleYStart = 80; const teamYStart = 60; const lineHeight = 30; const roundsPerRow = 3; const rowHeight = 300; + const result = {}; teamCombinations.forEach((roundTeams, roundIndex) => { + result[roundIndex] = [] const rowIndex = Math.floor(roundIndex / roundsPerRow); const colIndex = roundIndex % roundsPerRow; const roundXPosition = (canvasWidth / roundsPerRow) * colIndex + (canvasWidth / roundsPerRow) / 2; const roundYPosition = roundTitleYStart + rowIndex * rowHeight; ctx.font = `bold 18px "${font}"`; - ctx.textAlign = 'center'; + ctx.textAlign = "center"; ctx.fillText(`第 ${roundIndex + 1} 輪`, roundXPosition, roundYPosition); ctx.font = `16px "${font}"`; roundTeams.forEach((team, teamIndex) => { + result[roundIndex].push([team.teamA, team.teamB]) const teamYPosition = roundYPosition + teamYStart + teamIndex * lineHeight; if (teamIndex === 0) { - ctx.fillText('1號隊友', roundXPosition - 50, teamYPosition); - ctx.fillText('2號隊友', roundXPosition + 50, teamYPosition); + ctx.fillText("1號隊友", roundXPosition - 50, teamYPosition); + ctx.fillText("2號隊友", roundXPosition + 50, teamYPosition); } ctx.fillText(team.teamA, roundXPosition - 50, teamYPosition + lineHeight); ctx.fillText(team.teamB, roundXPosition + 50, teamYPosition + lineHeight); @@ -114,8 +126,67 @@ export function generateImage() { }); // 导出图片 - const buffer = canvas.toBuffer('image/png'); - // fs.writeFileSync('./team_combination.png', buffer); - console.log('圖片已生成!'); + const buffer = canvas.toBuffer("image/png"); + // fs.writeFileSync("./team_combination.png", buffer); + let dateTime: string = dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss"); + console.log(`${dateTime} [canvas] 圖片已生成!`); + + + const personnelStr: string = JSON.stringify(team); + const resultStr: string = JSON.stringify(result); + const query: string = String.Format("INSERT INTO `badminton` (time,personnel,battlecombination) VALUES ({0},'{1}','{2}') ON DUPLICATE KEY UPDATE personnel='{1}',battlecombination='{2}';" + , date, personnelStr, resultStr); + sqlSendQuery(query); + + const message = `\n${formatDate} 比賽隊伍`; + lineNotify({ + LINE_ACCESS_TOKEN: process.env.LINE_ACCESS_TOKEN, + message, + imageFile: buffer + }) return buffer; +} + +interface ICanvas { + date: string; + team: any; + rounds: number; +} + +function splitTeams(data: [number, string][]): { aTeam: string[], bTeam: string[] } { + const aTeam: string[] = []; + const bTeam: string[] = []; + + // 分离 boys 和 girls + data.forEach(([type, name]) => { + if (type === 1) { + aTeam.push(name); + } else if (type === 0) { + bTeam.push(name); + } + }); + + // 进行平衡 + while (aTeam.length > bTeam.length + 1) { + bTeam.push(aTeam.pop()!); // 移动一名男生到女生 + } + + // 平衡后可能还有空缺 + const missingBoys = aTeam.length; + const missingGirls = bTeam.length; + const balanceSize = Math.max(missingBoys, missingGirls); + + if (missingBoys > balanceSize) { + // 若男生过多,添加补充人员到女生 + for (let i = 0; i < missingBoys; i++) { + bTeam.push("那個"); + } + } else if (missingGirls > balanceSize) { + // 若女生过多,添加补充人员到男生 + for (let i = 0; i < missingGirls; i++) { + aTeam.push("那個"); + } + } + + return { aTeam, bTeam }; } \ No newline at end of file diff --git a/src/fonts/PMingLiU.ttf b/src/fonts/PMingLiU.ttf new file mode 100644 index 0000000..1a284f7 Binary files /dev/null and b/src/fonts/PMingLiU.ttf differ diff --git a/src/lineNotify.ts b/src/lineNotify.ts new file mode 100644 index 0000000..8772231 --- /dev/null +++ b/src/lineNotify.ts @@ -0,0 +1,35 @@ +import axios from "axios"; +import FormData from "form-data"; + +// lineNotify +export function lineNotify(props: ILineNotify) { + const { LINE_ACCESS_TOKEN, message, imageFile } = props; + let data = new FormData(); + data.append("message", message); + if (imageFile) { + // 添加圖像文件 + data.append("imageFile", imageFile, "image.jpg"); + } + + let config = { + method: "post", + maxBodyLength: Infinity, + url: "https://notify-api.line.me/api/notify", + headers: { + "Authorization": `Bearer ${LINE_ACCESS_TOKEN}`, + ...data.getHeaders() + }, + data: data + }; + + axios.request(config) + .then((response) => { + // console.log(JSON.stringify(response.data)); + }) + .catch((error) => { + console.log(error); + }); + +} + +interface ILineNotify { LINE_ACCESS_TOKEN: string, message: string, imageFile?: Buffer } \ No newline at end of file diff --git a/src/sql.ts b/src/sql.ts new file mode 100644 index 0000000..50ed179 --- /dev/null +++ b/src/sql.ts @@ -0,0 +1,36 @@ +import mysql from "mysql"; +import Tools from "./Tools"; + +export async function sqlSendQuery(query: string) { + try { + const connection: mysql.Connection = mysql.createConnection({ + host: process.env.DB_HOST, + port: +process.env.DB_PORT, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + connection.connect(); + + let resp: any = null; + let run: boolean = true; + connection.query(query, function (err: mysql.MysqlError, rows: any, fields: mysql.FieldInfo[]): void { + if (err) { + console.error(`${query} Error: \n${err.message}`); + run = false; + } + resp = rows; + run = false; + }); + while (run) { + await Tools.Sleep(100); + } + // 释放连接 + connection.end(); + return resp; + + } catch (error) { + console.error("MySQL 连接失败:", error); + return null; + } +}