[add] 第一版完成

This commit is contained in:
建喵 2024-09-11 09:05:40 +08:00
parent 82c94e8549
commit 14ab663c03
18 changed files with 1083 additions and 67 deletions

View File

@ -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
# DB----------------------------------------------
DB_HOST = 192.168.0.15
DB_PORT = 3307
DB_USER = jianmiau
DB_PASSWORD = VQ*ZetC7xcc9%dTW
DB_DATABASE = badminton

View File

@ -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
# DB----------------------------------------------
DB_HOST = 192.168.0.15
DB_PORT = 3307
DB_USER = jianmiau
DB_PASSWORD = VQ*ZetC7xcc9%dTW
DB_DATABASE = badminton

21
.vscode/launch.json vendored Normal file
View File

@ -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": [
"<node_internals>/**"
]
}
]
}

View File

@ -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
@ -41,3 +36,18 @@ COPY . .
# 指定啟動container後執行命令
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

308
package-lock.json generated
View File

@ -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"
}
}
}
}

View File

@ -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",

View File

@ -0,0 +1,116 @@
declare interface Array<T> {
/**
*
* @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[];
/**
* Array<cc.Component.EventHandler>forHoldButton使用
* 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;
}
});

View File

@ -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": {}
}

View File

@ -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位小數2200.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;
}
})

View File

@ -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;
}
}

View File

@ -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];
}
}
}

16
src/Engine/String.ts Normal file
View File

@ -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;
});
};

13
src/Tools.ts Normal file
View File

@ -0,0 +1,13 @@
/**
* Tools
*/
export default class Tools {
//#region Custom
public static Sleep(ms: number): Promise<any> {
return new Promise(resolve => setTimeout(resolve, ms));
}
//#endregion
}

View File

@ -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 服务器

View File

@ -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 };
}

BIN
src/fonts/PMingLiU.ttf Normal file

Binary file not shown.

35
src/lineNotify.ts Normal file
View File

@ -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 }

36
src/sql.ts Normal file
View File

@ -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;
}
}