├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── lerna.json ├── package.json └── packages ├── admin ├── .env.development ├── .env.production ├── .gitignore ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── index.html ├── jsconfig.json ├── package.json ├── public │ └── logo.png ├── src │ ├── App.vue │ ├── assets │ │ ├── login-back.png │ │ ├── login-left-image.png │ │ └── logo.png │ ├── components │ │ └── .gitkeep │ ├── config │ │ ├── app.js │ │ └── internal.js │ ├── i18n │ │ ├── en.json │ │ └── zh.json │ ├── layouts │ │ └── default │ │ │ ├── components │ │ │ └── sideMenu.vue │ │ │ └── index.vue │ ├── main.js │ ├── pages │ │ ├── index │ │ │ ├── index.vue │ │ │ └── route.js │ │ └── internal │ │ │ ├── admin │ │ │ ├── components │ │ │ │ └── editDataDrawer.vue │ │ │ ├── index.vue │ │ │ └── route.js │ │ │ ├── adminActionLog │ │ │ ├── index.vue │ │ │ └── route.js │ │ │ ├── adminLoginLog │ │ │ ├── index.vue │ │ │ └── route.js │ │ │ ├── api │ │ │ ├── index.vue │ │ │ └── route.js │ │ │ ├── appConfig │ │ │ ├── index.vue │ │ │ └── route.js │ │ │ ├── dict │ │ │ ├── components │ │ │ │ └── editDataDrawer.vue │ │ │ ├── index.vue │ │ │ └── route.js │ │ │ ├── dictCategory │ │ │ ├── components │ │ │ │ └── editDataDrawer.vue │ │ │ ├── index.vue │ │ │ └── route.js │ │ │ ├── login │ │ │ ├── index.vue │ │ │ └── route.js │ │ │ ├── mailLog │ │ │ ├── components │ │ │ │ └── sendMailDrawer.vue │ │ │ ├── index.vue │ │ │ └── route.js │ │ │ ├── menu │ │ │ ├── index.vue │ │ │ └── route.js │ │ │ ├── readme.md │ │ │ └── role │ │ │ ├── components │ │ │ └── editDataDrawer.vue │ │ │ ├── index.vue │ │ │ └── route.js │ ├── plugins │ │ ├── global.js │ │ ├── http.js │ │ ├── i18n.js │ │ ├── router.js │ │ └── storage.js │ ├── styles │ │ ├── global.scss │ │ ├── internal.scss │ │ └── variable.scss │ └── utils │ │ ├── index.js │ │ └── internal.js └── yite.config.js ├── api ├── .env.development ├── .env.production ├── .gitignore ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── addons │ └── weixin │ │ ├── apis │ │ └── pay │ │ │ ├── _meta.js │ │ │ └── select.js │ │ └── tables │ │ └── pay.js ├── apis │ └── example │ │ ├── _ffffff.js │ │ ├── _meta.js │ │ ├── delete.js │ │ ├── detail.js │ │ ├── insert.js │ │ ├── select.js │ │ └── update.js ├── config │ ├── custom.js │ └── menu.js ├── funpi.js ├── package.json ├── plugins │ └── .gitkeep ├── pm2.config.cjs ├── public │ └── .gitkeep ├── scripts │ ├── checkTable.js │ └── syncMysql.js └── tables │ └── example.js ├── cli ├── LICENSE ├── README.md └── package.json ├── funpi ├── LICENSE ├── README.md ├── apis │ ├── admin │ │ ├── _meta.js │ │ ├── adminActionLogSelectPage.js │ │ ├── adminDelete.js │ │ ├── adminInsert.js │ │ ├── adminLogin.js │ │ ├── adminLoginLogSelectPage.js │ │ ├── adminSelectPage.js │ │ ├── adminUpdate.js │ │ ├── apiSelectAll.js │ │ ├── apiSelectPage.js │ │ ├── getApis.js │ │ ├── getMenus.js │ │ ├── mailSelectPage.js │ │ ├── menuSelectAll.js │ │ ├── menuSelectPage.js │ │ ├── roleDelete.js │ │ ├── roleDetail.js │ │ ├── roleInsert.js │ │ ├── roleSelectAll.js │ │ ├── roleSelectPage.js │ │ └── roleUpdate.js │ ├── dict │ │ ├── _meta.js │ │ ├── categoryDelete.js │ │ ├── categoryInsert.js │ │ ├── categorySelectAll.js │ │ ├── categorySelectPage.js │ │ ├── categoryUpdate.js │ │ ├── dictDelete.js │ │ ├── dictInsert.js │ │ ├── dictSelectAll.js │ │ ├── dictSelectPage.js │ │ └── dictUpdate.js │ └── tool │ │ ├── _meta.js │ │ ├── sendMail.js │ │ └── tokenCheck.js ├── bootstrap │ ├── auth.js │ ├── cors.js │ ├── mail.js │ ├── mysql.js │ ├── redis.js │ ├── syncApi.js │ ├── syncMenu.js │ ├── syncRole.js │ ├── tool.js │ ├── upload.js │ └── xmlParse.js ├── config │ ├── env.js │ ├── http.js │ ├── menu.js │ ├── path.js │ └── role.js ├── funpi.js ├── package.json ├── plugins │ ├── jwt.js │ ├── logger.js │ └── swagger.js ├── schema │ ├── env.js │ ├── menu.js │ └── table.js ├── scripts │ ├── checkTable.js │ └── syncMysql.js ├── tables │ ├── admin.js │ ├── adminActionLog.js │ ├── adminLoginLog.js │ ├── api.js │ ├── dict.js │ ├── dictCategory.js │ ├── mailLog.js │ ├── menu.js │ └── role.js ├── todo.md └── utils │ ├── ajvZh.js │ ├── check.js │ ├── colors.js │ └── index.js └── vscode ├── LICENSE ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | esdata 6 | data 7 | .cache 8 | lab 9 | labs 10 | cache 11 | .VSCodeCounter 12 | oclif.manifest.json 13 | dist 14 | report.html 15 | dist-ssr 16 | *.local 17 | .vscode-test/ 18 | *.vsix 19 | out 20 | CHANGELOG.md 21 | note.md 22 | .changeset 23 | addons2 24 | pnpm-lock.yaml 25 | bun.lock 26 | 27 | # Editor directories and files 28 | .vscode/* 29 | .cache 30 | .yicode/auto-imports.d.ts 31 | !.vscode/extensions.json 32 | .idea 33 | .DS_Store 34 | *.suo 35 | *.ntvs* 36 | *.njsproj 37 | *.sln 38 | *.sw? 39 | 40 | # Runtime data 41 | pids 42 | *.pid 43 | *.seed 44 | 45 | # Directory for instrumented libs generated by jscoverage/JSCover 46 | lib-cov 47 | 48 | # Coverage directory used by tools like istanbul 49 | coverage 50 | 51 | # nyc test coverage 52 | .nyc_output 53 | 54 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 55 | .grunt 56 | 57 | # node-waf configuration 58 | .lock-wscript 59 | 60 | # Compiled binary addons (http://nodejs.org/api/addons.html) 61 | build/Release 62 | 63 | # Dependency directories 64 | node_modules 65 | jspm_packages 66 | 67 | # Optional npm cache directory 68 | .npm 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # 0x 74 | profile-* 75 | 76 | # mac files 77 | .DS_Store 78 | 79 | # vim swap files 80 | *.swp 81 | 82 | # webstorm 83 | .idea 84 | 85 | # vscode 86 | .vscode 87 | *code-workspace 88 | 89 | # clinic 90 | profile* 91 | *clinic* 92 | *flamegraph* 93 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com 2 | # registry=https://registry.npmjs.org 3 | link-workspace-packages=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | LICENSE 2 | LICENSE.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 1024, 7 | "bracketSpacing": true, 8 | "useTabs": false, 9 | "arrowParens": "always", 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # funpi 是什么? 2 | 3 | 中文名称 `放屁` 接口框架。 4 | 5 | 像放屁一样简单又自然的 `Node.js` 接口开发框架。 6 | 7 | > 注意:本项目 `v7` 为内测版,`v8` 才是公测版,谨慎使用。 8 | > 9 | > 自 `v7.15.0` 版本开始,本项目仅支持 [Bun](https://bun.sh),不再支持 `Node.js`。 10 | 11 | ### 仓库地址 12 | 13 | [github - https://github.com/chenbimo/funpi](https://github.com/chenbimo/funpi) 14 | 15 | ### 使用教程 16 | 17 | [funpi(放屁)使用文档](https://sourl.cn/bUq25t) 18 | 19 | ### 作者介绍 20 | 21 | [前端之虎陈随易 https://chensuiyi.me](https://chensuiyi.me) 22 | 23 | ### 演示地址 24 | 25 | - [https://funpi-demo.yicode.tech](https://funpi-demo.yicode.tech) 26 | 27 | ### 功能特点 28 | 29 | - ✅ 只需 `简单配置`,即可快速上手开发。 30 | - ✅ 自动生成 `接口文档`,方便前后端对接。 31 | - ✅ 自带 `权限`、`角色`、`管理`、`日志`、`菜单`、`接口`、`字典` 等基础功能。 32 | - ✅ 自带 `邮件发送`,`文件上传` 等功能。 33 | - ✅ 自带 `日志打印` 和 `日志分割` 功能。 34 | - ✅ 自带 `jwt` 鉴权机制。 35 | - ✅ 自带 `登录日志`,`邮件日志` 等功能。 36 | - ✅ 自带配套的后台管理系统 `yiadmin`,30 分钟搭建一个后台管理系统。 37 | - ✅ 默认已处理 `跨域` 问题,无需再为跨域担心。 38 | - ✅ 优先使用 `缓存`,提高应用性能。 39 | - ✅ 默认提供 `静态文件托管` 功能。 40 | - ✅ 可以 `一键更新` 后台管理系统。 41 | - ✅ 全面的 `接口参数验证` 功能,极大减少安全隐患。 42 | - ✅ 提供 `数据库表字段设计` 和 `表结构同步` 功能。 43 | 44 | ### 功能限制 45 | 46 | 本框架做了很多约束,减少自由度,增加确定度,稳定度。 47 | 48 | - ❎ 仅支持 `Bun`,不支持 `Node.js`,`Deno` 等。 49 | - ❎ 仅支持 `单机部署`,使用 `pm2` 管理。 50 | - ❎ 仅支持 `单角色权限`。 51 | - ❎ 仅支持 `Mysql` 关系数据库。 52 | - ❎ 仅支持 `Redis` 缓存数据库。 53 | - ❎ 仅支持 `POST` 和 `GET` 请求方法。 54 | - ❎ 仅支持 `整数`、`浮点数`、`文本`、`字符串` 这四种数据库字段类型。 55 | - ❎ 不支持 `分库分表`。 56 | - ❎ 不支持 `Docker` 部署,请自行研究。 57 | - ❎ 不支持 `分布式部署`。 58 | - ❎ 不支持 `Restful` 规范,不认同 `Restful` 规范,不使用 `Restful` 规范。 59 | 60 | ### 付费插件 61 | 62 | - `微信扫码插件`,登录注册,需要提供微信公众号。 63 | - `在线人数统计插件`,提供 `踢人`,`拉黑` 等功能。 64 | - `微信支付插件`,支持 `多产品`、`折扣`、`优惠` 等功能。 65 | 66 | ### 注意事项 67 | 68 | - 与本项目逻辑、BUG、建议相关的问题,请联系作者无偿 `免费处理`。 69 | - 与本项目无关的业务、功能、需求、部署相关的问题,请联系作者 `有偿咨询`。 70 | 71 | ### 实际效果 72 | 73 | 使用 `funpi` + `yiadmin` 驱动的,免费且开源的后台管理系统。 74 | 75 | #### 📄 登录页面 76 | 77 | ![picture 0](https://static.yicode.tech/images/202311/20231126000719.png) 78 | 79 | #### 📄 菜单页面 80 | 81 | ![picture 2](https://static.yicode.tech/images/202311/20231126000809.png) 82 | 83 | #### 📄 接口页面 84 | 85 | ![picture 3](https://static.yicode.tech/images/202311/20231126000833.png) 86 | 87 | #### 📄 角色页面 88 | 89 | ![picture 4](https://static.yicode.tech/images/202311/20231126000913.png) 90 | 91 | #### 📄 登录日志 92 | 93 | ![picture 5](https://static.yicode.tech/images/202311/20231126000935.png) 94 | 95 | #### 📄 邮件日志 96 | 97 | ![picture 6](https://static.yicode.tech/images/202311/20231126001012.png) 98 | 99 | ### 版权说明 100 | 101 | `funpi(放屁)` 使用 `Apache 2.0` 协议开源 102 | 103 | > 一句话总结:开源不等于放弃版权,不可侵犯原作者版权,改动处要做说明,可以闭源使用。 104 | 105 | 拥有版权(Copyright)意味着你对你开发的软件及其源代码拥有著作权,所有权和其他法定权利,使用一个开源协议并不意味着放弃版权。 106 | 107 | 在 `Apache 2.0` 协议许可下,您可以: 108 | 109 | - **商业化使用**(这意味着,您可以出于商业目的使用这些源代码) 110 | - **再分发**(这意味着,您可以将源代码副本传输给其他任何人) 111 | - **修改**(这意味着,您可以修改源代码) 112 | - **专利使用**(这意味着,版权人明确声明授予您专利使用权) 113 | - **私人使用**(这意味着,您可以出于一切目的私下使用和修改源代码) 114 | 115 | 唯须遵守以下条款: 116 | 117 | - **协议和版权通知**(这意味着,软件中必须包含许可证和版权声明的副本) 118 | - **状态更改说明**(如果您更改软件,您应当提供适当的说明) 119 | 120 | 除此之外,该软件: 121 | 122 | - **提供责任限制**(版权人声明不对使用者造成的任何损失负责) 123 | - **限制商标使用** (不能使用版权人的商标) 124 | - **不提供任何担保**(版权人声明不为该软件的品质提供任何担保) 125 | 126 | 进一步说明: 127 | 128 | 1. 本软件又叫本 **作品**,可以是源码,也可以是编译或转换后的其他形式。**衍生作品** 是在本作品的基础上修改后的有原创性的工作成果。本作品的 **贡献者** 包括许可人和其他提交了贡献的人,以下统称 **我**。 129 | 2. 我授予你权利:你可以免费复制、使用、修改、再许可、分发本作品及衍生作品(可以不用公开源码)。 130 | 3. 如果本软件涉及我的专利(或潜在专利),我在此授予你专利许可,你可以永久性地免费使用此专利,用于制作、使用、出售、转让本作品。如果你哪天居然告本作品侵权,你的专利许可在你告我那天被收回。 131 | 4. 你在复制和分发本作品或衍生作品时,要满足以下条件。 132 | 133 | - 带一份本许可证。 134 | - 如果你修改了什么,要在改动的文件中有明显的修改声明。 135 | - 如果你以源码形式分发,你必须保留本作品的版权、专利、商标和归属声明。 136 | - 如果本作品带了 **NOTICE** 文件,你就得带上 **NOTICE** 文件中包含的归属声明。即便你的发布是不带源码的,你也得带上此文件,并在作品某处予以展示。 137 | - 你可以对自己的修改添加版权说明。对于你的修改或者整个衍生作品,你可以使用不同的许可,但你对本作品的使用、复制和分发等,必须符合本许可证规定。 138 | 139 | 5. 你提交贡献就表明你默认遵守本许可的条款和条件。当然,你可以和我签订另外的专门的条款。 140 | 6. 你不许使用我的商品名、商标、服务标志或产品名。 141 | 7. 本作品是 **按原样**(AS IS)提供的,没有任何保证啊,你懂的。 142 | 8. 我可不负任何责任。除非我书面同意,或者法律有这样的要求(例如对故意和重大过失行为负责)。 143 | 9. 你可以向别人提供保证,你可以向别人收费,但那都是你的事,别给我惹麻烦。 144 | 145 | 注意以上的 **我**,既包含了许可人,也包含了每位 **贡献者**。 146 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7.20.27", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "npmClient": "bun", 7 | "command": { 8 | "version": { 9 | "allowBranch": [ 10 | "main", 11 | "master" 12 | ], 13 | "changelog": false, 14 | "gitTagVersion": true, 15 | "yes": true, 16 | "forcePublish": true, 17 | "syncWorkspaceLock": true, 18 | "exact": true, 19 | "push": false 20 | }, 21 | "publish": { 22 | "ignoreScripts": false, 23 | "yes": true 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "funpi", 3 | "version": "1.0.0", 4 | "description": "放屁接口 + 后台管理", 5 | "main": "funpi.js", 6 | "type": "module", 7 | "publishConfig": { 8 | "access": "restricted" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/chenbimo/funpi.git" 13 | }, 14 | "scripts": { 15 | "r": "lerna publish" 16 | }, 17 | "author": "chensuiyi ", 18 | "homepage": "https://chensuiyi.me", 19 | "workspaces": [ 20 | "packages/*" 21 | ], 22 | "devDependencies": { 23 | "lerna": "^8.2.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/admin/.env.development: -------------------------------------------------------------------------------- 1 | VITE_HOST="http://127.0.0.1:3000/api" 2 | VITE_NAMESPACE="dev.funpi-admin" -------------------------------------------------------------------------------- /packages/admin/.env.production: -------------------------------------------------------------------------------- 1 | VITE_HOST="https://funpi-api.yicode.tech/api" 2 | VITE_NAMESPACE="prod.funpi-admin" -------------------------------------------------------------------------------- /packages/admin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | esdata 6 | data 7 | .cache 8 | lab 9 | labs 10 | cache 11 | .VSCodeCounter 12 | oclif.manifest.json 13 | dist 14 | report.html 15 | dist-ssr 16 | *.local 17 | .vscode-test/ 18 | *.vsix 19 | out 20 | CHANGELOG.md 21 | note.md 22 | .changeset 23 | 24 | # Editor directories and files 25 | .vscode/* 26 | .cache 27 | .yicode/auto-imports.d.ts 28 | !.vscode/extensions.json 29 | .idea 30 | .DS_Store 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | *.sw? 36 | *.lock 37 | *lock.* 38 | 39 | # Runtime data 40 | pids 41 | *.pid 42 | *.seed 43 | 44 | # Directory for instrumented libs generated by jscoverage/JSCover 45 | lib-cov 46 | 47 | # Coverage directory used by tools like istanbul 48 | coverage 49 | 50 | # nyc test coverage 51 | .nyc_output 52 | 53 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 54 | .grunt 55 | 56 | # node-waf configuration 57 | .lock-wscript 58 | 59 | # Compiled binary addons (http://nodejs.org/api/addons.html) 60 | build/Release 61 | 62 | # Dependency directories 63 | node_modules 64 | jspm_packages 65 | 66 | # Optional npm cache directory 67 | .npm 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # 0x 73 | profile-* 74 | 75 | # mac files 76 | .DS_Store 77 | 78 | # vim swap files 79 | *.swp 80 | 81 | # webstorm 82 | .idea 83 | 84 | # vscode 85 | .vscode 86 | *code-workspace 87 | 88 | # clinic 89 | profile* 90 | *clinic* 91 | *flamegraph* 92 | -------------------------------------------------------------------------------- /packages/admin/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com 2 | # registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /packages/admin/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 1024, 7 | "bracketSpacing": true, 8 | "useTabs": false, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /packages/admin/README.md: -------------------------------------------------------------------------------- 1 | # funpi 是什么? 2 | 3 | 中文名称 `放屁` 接口框架。 4 | 5 | 像放屁一样简单又自然的 `Node.js` 接口开发框架。 6 | 7 | > 注意:本项目 `v6` 版本为自用,`v7` 为内测版,`v8` 才是公测版。 8 | 9 | ### 仓库地址 10 | 11 | [github - https://github.com/chenbimo/funpi](https://github.com/chenbimo/funpi) 12 | 13 | ### 作者介绍 14 | 15 | [前端之虎陈随易 https://chensuiyi.me](https://chensuiyi.me) 16 | 17 | ### 文档教程 18 | 19 | 请到 [前端之虎陈随易 https://chensuiyi.me](https://chensuiyi.me) 网站查看。 20 | -------------------------------------------------------------------------------- /packages/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 易管理 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/admin/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "baseUrl": "./", 6 | "paths": { 7 | "@/*": ["src/*"] 8 | } 9 | }, 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@funpi/admin", 3 | "version": "7.20.27", 4 | "description": "FunPi(放屁) - 后台管理", 5 | "sideEffects": true, 6 | "type": "module", 7 | "private": false, 8 | "license": "Apache-2.0", 9 | "publishConfig": { 10 | "access": "public", 11 | "registry": "https://registry.npmjs.org" 12 | }, 13 | "author": "chensuiyi ", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/chenbimo/yicode.git" 17 | }, 18 | "homepage": "https://chensuiyi.me", 19 | "scripts": { 20 | "dev": "yite --command=dev --envfile=development --workdir=./", 21 | "build": "yite --command=build --envfile=production --workdir=./", 22 | "update:template": "yite --command=update --project-type=yiadmin" 23 | }, 24 | "keywords": [ 25 | "lodash", 26 | "utils", 27 | "helper", 28 | "help" 29 | ], 30 | "files": [ 31 | "public/", 32 | "src/", 33 | ".env.development", 34 | ".env.production", 35 | ".gitignore", 36 | ".prettier", 37 | ".npmrc", 38 | "index.html", 39 | "jsconfig.json", 40 | "LICENSE", 41 | "package.json", 42 | "README.md", 43 | "yite.config.js" 44 | ], 45 | "dependencies": { 46 | "@arco-design/web-vue": "^2.57.0", 47 | "@arco-plugins/vite-vue": "^1.4.5", 48 | "@iconify/json": "^2.2.343", 49 | "axios": "^1.9.0", 50 | "date-fns": "^4.1.0", 51 | "es-toolkit": "^1.38.0", 52 | "js-md5": "^0.8.3", 53 | "store2": "^2.14.4", 54 | "yite-cli": "^4.7.0" 55 | }, 56 | "gitHead": "1c28c0de7c0af8aa4582c45ab2d98e66c597c7a1" 57 | } 58 | -------------------------------------------------------------------------------- /packages/admin/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenbimo/funpi/d81b828cb65d51c31cbc6200c283b2d81caf8fd6/packages/admin/public/logo.png -------------------------------------------------------------------------------- /packages/admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /packages/admin/src/assets/login-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenbimo/funpi/d81b828cb65d51c31cbc6200c283b2d81caf8fd6/packages/admin/src/assets/login-back.png -------------------------------------------------------------------------------- /packages/admin/src/assets/login-left-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenbimo/funpi/d81b828cb65d51c31cbc6200c283b2d81caf8fd6/packages/admin/src/assets/login-left-image.png -------------------------------------------------------------------------------- /packages/admin/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenbimo/funpi/d81b828cb65d51c31cbc6200c283b2d81caf8fd6/packages/admin/src/assets/logo.png -------------------------------------------------------------------------------- /packages/admin/src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenbimo/funpi/d81b828cb65d51c31cbc6200c283b2d81caf8fd6/packages/admin/src/components/.gitkeep -------------------------------------------------------------------------------- /packages/admin/src/config/app.js: -------------------------------------------------------------------------------- 1 | export const $AppConfig = { 2 | appName: '易管理' 3 | }; 4 | -------------------------------------------------------------------------------- /packages/admin/src/config/internal.js: -------------------------------------------------------------------------------- 1 | export const $InternalConfig = { 2 | // 用户令牌 3 | token: $Storage.local.get('token') || '', 4 | // 用户数据 5 | userData: $Storage.local.get('userData') || {}, 6 | // 表格边框 7 | tableBordered: { 8 | cell: true 9 | }, 10 | // 表格滚动 11 | tableScroll: { 12 | x: '100%', 13 | y: '100%', 14 | maxHeight: '100%' 15 | }, 16 | modalShortWidth: 600, 17 | modalLongWidth: 1200, 18 | // 抽屉默认宽度 19 | drawerWidth: 400, 20 | // 每页显示数量 21 | pageLimit: 30 22 | }; 23 | -------------------------------------------------------------------------------- /packages/admin/src/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test" 3 | } 4 | -------------------------------------------------------------------------------- /packages/admin/src/i18n/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "测试" 3 | } 4 | -------------------------------------------------------------------------------- /packages/admin/src/layouts/default/components/sideMenu.vue: -------------------------------------------------------------------------------- 1 | 12 | 45 | -------------------------------------------------------------------------------- /packages/admin/src/main.js: -------------------------------------------------------------------------------- 1 | import App from '@/App.vue'; 2 | import '@arco-design/web-vue/dist/arco.css'; 3 | import 'virtual:uno.css'; 4 | 5 | const $App = createApp(App); 6 | const $Pinia = createPinia(); 7 | 8 | $App.use($Router); 9 | $App.use($Pinia); 10 | $App.use($I18n); 11 | 12 | $App.mount('#app'); 13 | -------------------------------------------------------------------------------- /packages/admin/src/pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | 28 | 32 | -------------------------------------------------------------------------------- /packages/admin/src/pages/index/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/admin/components/editDataDrawer.vue: -------------------------------------------------------------------------------- 1 | 35 | 139 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/admin/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/adminActionLog/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 96 | 97 | 101 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/adminActionLog/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/adminLoginLog/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 100 | 101 | 105 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/adminLoginLog/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/api/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 94 | 95 | 99 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/api/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/appConfig/index.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 120 | 121 | 125 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/appConfig/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/dict/components/editDataDrawer.vue: -------------------------------------------------------------------------------- 1 | 44 | 148 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/dict/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/dictCategory/components/editDataDrawer.vue: -------------------------------------------------------------------------------- 1 | 30 | 110 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/dictCategory/index.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 148 | 149 | 153 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/dictCategory/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/login/route.js: -------------------------------------------------------------------------------- 1 | export default { 2 | layout: false 3 | }; 4 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/mailLog/components/sendMailDrawer.vue: -------------------------------------------------------------------------------- 1 | 32 | 105 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/mailLog/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 112 | 113 | 117 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/mailLog/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/menu/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 100 | 101 | 105 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/menu/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/readme.md: -------------------------------------------------------------------------------- 1 | 此目录下的文件不要改动!!! 2 | -------------------------------------------------------------------------------- /packages/admin/src/pages/internal/role/route.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/admin/src/plugins/global.js: -------------------------------------------------------------------------------- 1 | export const useGlobal = defineStore('global', () => { 2 | // 全局数据 3 | const $GlobalData = $ref({ 4 | // 内置配置,不要修改 5 | ...$InternalConfig 6 | }); 7 | 8 | // 全局计算数据 9 | const $GlobalComputed = {}; 10 | 11 | // 全局方法 12 | const $GlobalMethod = {}; 13 | 14 | return { 15 | $GlobalData, 16 | $GlobalComputed, 17 | $GlobalMethod 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /packages/admin/src/plugins/http.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Message } from '@arco-design/web-vue'; 3 | 4 | const $Http = axios.create({ 5 | method: 'POST', 6 | baseURL: import.meta.env.VITE_HOST, 7 | timeout: 1000 * 60, 8 | withCredentials: false, 9 | responseType: 'json', 10 | responseEncoding: 'utf8', 11 | headers: { 12 | 'Content-Type': 'application/json; charset=utf-8' 13 | }, 14 | transformRequest: [ 15 | (data, headers) => { 16 | const data2 = {}; 17 | for (let key in data) { 18 | if (Object.prototype.hasOwnProperty.call(data, key) && [null, undefined].includes(data[key]) === false) { 19 | data2[key] = data[key]; 20 | } 21 | } 22 | return JSON.stringify(data2); 23 | } 24 | ] 25 | }); 26 | // 添加请求拦截器 27 | $Http.interceptors.request.use( 28 | function (config) { 29 | const token = $Storage.local.get('token'); 30 | if (token) { 31 | config.headers.authorization = 'Bearer ' + token; 32 | } 33 | return config; 34 | }, 35 | function (err) { 36 | return Promise.reject(err); 37 | } 38 | ); 39 | 40 | // 添加响应拦截器 41 | $Http.interceptors.response.use( 42 | function (res) { 43 | if (res.data.code === 0) { 44 | return Promise.resolve(res.data); 45 | } 46 | if (res.data.symbol === 'NOT_LOGIN') { 47 | location.href = location.origin + '/#/internal/login'; 48 | } 49 | return Promise.reject(res.data); 50 | }, 51 | function (err) { 52 | Message.error(err.message); 53 | return Promise.reject(err); 54 | } 55 | ); 56 | export { $Http }; 57 | -------------------------------------------------------------------------------- /packages/admin/src/plugins/i18n.js: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | import { yiteMessages } from 'virtual:yite-messages'; 3 | 4 | const $I18n = createI18n({ 5 | legacy: false, 6 | locale: 'zh', 7 | messages: await yiteMessages() 8 | }); 9 | 10 | export { $I18n }; 11 | -------------------------------------------------------------------------------- /packages/admin/src/plugins/router.js: -------------------------------------------------------------------------------- 1 | import { yiteRoutes } from 'virtual:yite-router'; 2 | 3 | // 创建路由 4 | const $Router = createRouter({ 5 | routes: yiteRoutes(), 6 | history: createWebHashHistory() 7 | }); 8 | 9 | export { $Router }; 10 | -------------------------------------------------------------------------------- /packages/admin/src/plugins/storage.js: -------------------------------------------------------------------------------- 1 | import store2 from 'store2'; 2 | const $Storage = store2.namespace(import.meta.env.VITE_NAMESPACE); 3 | 4 | // 提供给手动导入使用 5 | export { $Storage }; 6 | -------------------------------------------------------------------------------- /packages/admin/src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @use './internal.scss' as *; 2 | -------------------------------------------------------------------------------- /packages/admin/src/styles/internal.scss: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | border: 0; 4 | margin: 0; 5 | outline: 0; 6 | box-sizing: border-box; 7 | user-select: none; 8 | -webkit-appearance: none; 9 | } 10 | // ::-webkit-scrollbar { 11 | // width: 10px; 12 | // height: 10px; 13 | // } 14 | // ::-webkit-scrollbar-track { 15 | // border-radius: 5px; 16 | // } 17 | // ::-webkit-scrollbar-track-piece { 18 | // border-radius: 5px; 19 | // } 20 | // ::-webkit-scrollbar-thumb { 21 | // border-radius: 5px; 22 | // border: 3px solid transparent; 23 | // background-color: rgba(190, 190, 190, 0.4); 24 | // background-clip: padding-box; 25 | // } 26 | 27 | html, 28 | body { 29 | font-size: 14px; 30 | font-family: -apple-system, system-ui, 'Segoe UI', 'Roboto', 'Ubuntu', 'Cantarell', 'Noto Sans', sans-serif, 'BlinkMacSystemFont', 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial !important; 31 | } 32 | 33 | a { 34 | color: inherit; 35 | text-decoration: none; 36 | display: inline-block; 37 | } 38 | 39 | .app { 40 | position: fixed; 41 | top: 0; 42 | right: 0; 43 | bottom: 0; 44 | left: 0; 45 | } 46 | 47 | .my-modal-class { 48 | .bodyer { 49 | max-height: 70vh; 50 | overflow-y: auto; 51 | overflow-x: hidden; 52 | } 53 | } 54 | 55 | .menu-area { 56 | .arco-menu .arco-icon { 57 | margin-right: 0 !important; 58 | } 59 | } 60 | 61 | .link { 62 | color: #2588ff; 63 | } 64 | 65 | .bg-contain, 66 | .bg-cover { 67 | background-repeat: no-repeat; 68 | background-position: center center; 69 | } 70 | .bg-contain { 71 | background-size: contain; 72 | } 73 | .bg-cover { 74 | background-size: cover; 75 | } 76 | 77 | .bodyer-modal { 78 | max-height: 60vh; 79 | overflow-y: auto; 80 | overflow-x: hidden; 81 | } 82 | 83 | .common-badge { 84 | display: flex; 85 | height: 24px; 86 | line-height: 24px; 87 | background-color: #eee; 88 | border-radius: 2px; 89 | overflow: hidden; 90 | font-size: 14px; 91 | .label { 92 | padding: 0 8px; 93 | background-color: #165dff; 94 | color: #fff; 95 | } 96 | .value { 97 | padding: 0 8px; 98 | } 99 | } 100 | 101 | .page-full { 102 | position: absolute; 103 | top: 15px; 104 | right: 15px; 105 | bottom: 10px; 106 | left: 15px; 107 | .page-action { 108 | position: absolute; 109 | top: 0; 110 | left: 0; 111 | right: 0; 112 | height: 34px; 113 | display: flex; 114 | justify-content: space-between; 115 | .left { 116 | display: flex; 117 | align-items: center; 118 | } 119 | .right { 120 | display: flex; 121 | align-items: center; 122 | } 123 | } 124 | .page-table { 125 | position: absolute; 126 | top: 34px; 127 | left: 0; 128 | right: 0; 129 | bottom: 30px; 130 | padding: 10px 0; 131 | &.no-action { 132 | top: 0px; 133 | } 134 | &.no-page { 135 | bottom: 0px; 136 | } 137 | } 138 | .page-page { 139 | position: absolute; 140 | bottom: 0; 141 | left: 0; 142 | right: 0; 143 | height: 30px; 144 | display: flex; 145 | justify-content: space-between; 146 | } 147 | } 148 | 149 | .delete-modal-class { 150 | .arco-modal-body { 151 | text-align: center; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /packages/admin/src/styles/variable.scss: -------------------------------------------------------------------------------- 1 | $layout-header-height: 54px; 2 | -------------------------------------------------------------------------------- /packages/admin/src/utils/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenbimo/funpi/d81b828cb65d51c31cbc6200c283b2d81caf8fd6/packages/admin/src/utils/index.js -------------------------------------------------------------------------------- /packages/admin/src/utils/internal.js: -------------------------------------------------------------------------------- 1 | import { format, formatDistanceToNow } from 'date-fns'; 2 | import { zhCN } from 'date-fns/locale'; 3 | 4 | // 获取资源 5 | export function utilInternalAssets(name) { 6 | return new URL(`../assets/${name}`, import.meta.url).href; 7 | } 8 | 9 | // 树结构遍历 10 | export const utilTreeTraverse = (tree, mapFunction) => { 11 | function preorder(node, index, parent) { 12 | const newNode = Object.assign({}, mapFunction(node, index, parent)); 13 | 14 | if ('children' in node) { 15 | newNode.children = node.children.map(function (child, index) { 16 | return preorder(child, index, node); 17 | }); 18 | } 19 | 20 | return newNode; 21 | } 22 | 23 | return preorder(tree, null, null); 24 | }; 25 | 26 | export const utilArrayToTree = (arrs, id = 'id', pid = 'pid', children = 'children', forceChildren = true) => { 27 | // 输入验证 28 | if (!Array.isArray(arrs) || arrs.length === 0) { 29 | return []; 30 | } 31 | 32 | // 使用 Map 存储项目,并创建副本避免修改原始数据 33 | const idMap = new Map(); 34 | arrs.forEach((item) => { 35 | // 创建每个项目的副本 36 | idMap.set(item[id], { ...item }); 37 | }); 38 | 39 | const treeData = []; 40 | 41 | // 构建树结构 42 | idMap.forEach((item) => { 43 | const parentId = item[pid]; 44 | const parent = idMap.get(parentId); 45 | 46 | if (parent) { 47 | // 添加到父项的子列表 48 | if (!parent[children]) { 49 | parent[children] = []; 50 | } 51 | parent[children].push(item); 52 | } else { 53 | // 根节点 54 | if (forceChildren && !item[children]) { 55 | item[children] = []; 56 | } 57 | treeData.push(item); 58 | } 59 | }); 60 | 61 | return treeData; 62 | }; 63 | 64 | /** 65 | * 转换相对时间 66 | * @alias yd_datetime_relativeTime 67 | * @category datetime 68 | * @param {Array | object} data 数组或对象 69 | * @returns {object} 返回转换后的相对时间 70 | * @author 陈随易 71 | * @example yd_datetime_relativeTime([]) 72 | */ 73 | export const utilRelativeTime = (data) => { 74 | // 转换相对时间 75 | const _convertTime = (obj) => { 76 | try { 77 | const item = {}; 78 | for (let key in obj) { 79 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 80 | const value = obj[key]; 81 | if (key.endsWith('_at')) { 82 | let key1 = key.replace('_at', '_at1'); 83 | let key2 = key.replace('_at', '_at2'); 84 | let dt = new Date(value); 85 | if (value !== 0) { 86 | item[key] = value; 87 | item[key1] = format(dt, 'yyyy-MM-dd HH:mm:ss'); 88 | item[key2] = formatDistanceToNow(dt, { locale: zhCN, addSuffix: true }); 89 | } else { 90 | item[key] = ''; 91 | } 92 | } else { 93 | item[key] = value; 94 | } 95 | } 96 | } 97 | 98 | return item; 99 | } catch (err) { 100 | console.log('🚀 ~ err:', err); 101 | } 102 | }; 103 | // 如果是数组 104 | if (Array.isArray(data)) { 105 | return data.map((item) => { 106 | return _convertTime(item); 107 | }); 108 | } 109 | 110 | // 如果是对象 111 | return _convertTime(data); 112 | }; 113 | -------------------------------------------------------------------------------- /packages/admin/yite.config.js: -------------------------------------------------------------------------------- 1 | import { vitePluginForArco } from '@arco-plugins/vite-vue'; 2 | export const yiteConfig = { 3 | devtool: false, 4 | // 自动导入解析 5 | autoImport: { 6 | resolvers: [ 7 | { 8 | name: 'ArcoResolver', 9 | options: {} 10 | } 11 | ], 12 | imports: [ 13 | { 14 | '@arco-design/web-vue': [ 15 | // 16 | 'Message', 17 | 'Modal', 18 | 'Notification', 19 | 'Drawer' 20 | ] 21 | } 22 | ] 23 | }, 24 | // 自动组件解析 25 | autoComponent: { 26 | resolvers: [ 27 | { 28 | name: 'ArcoResolver', 29 | options: { 30 | sideEffect: true 31 | } 32 | } 33 | ] 34 | }, 35 | // webpack 配置 36 | viteConfig: { 37 | plugins: [ 38 | vitePluginForArco({ 39 | style: 'css' 40 | }) 41 | ], 42 | optimizeDeps: { 43 | include: [ 44 | // 45 | 'es-toolkit/compat', 46 | 'es-toolkit', 47 | 'vue-i18n', 48 | 'js-md5', 49 | 'axios', 50 | 'date-fns', 51 | 'date-fns/locale', 52 | '@arco-design/web-vue/es/icon', 53 | 'store2', 54 | '@arco-design/web-vue' 55 | ] 56 | } 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /packages/api/.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV="development" 2 | # 项目名称 3 | APP_NAME="易接口" 4 | APP_PORT=3000 5 | # MD5加密盐 6 | MD5_SALT="funpi123456" 7 | # 监听端口 8 | LISTEN_HOST="127.0.0.1" 9 | # 开发管理员密码 10 | DEV_PASSWORD="funpi123456" 11 | # 请求体大小 12 | BODY_LIMIT=10 13 | # 参数检查 14 | PARAMS_CHECK=0 15 | # 日志等级 16 | LOG_LEVEL="warn" 17 | # 时区 18 | TIMEZONE="Asia/Shanghai" 19 | # mysql 配置 20 | MYSQL_HOST="127.0.0.1" 21 | MYSQL_PORT=3306 22 | MYSQL_DB="funpi_demo" 23 | MYSQL_USERNAME="root" 24 | MYSQL_PASSWORD="root" 25 | TABLE_PRIMARY_KEY="default" 26 | # redis 配置 27 | REDIS_HOST="127.0.0.1" 28 | REDIS_PORT=6379 29 | REDIS_USERNAME="" 30 | REDIS_PASSWORD="" 31 | REDIS_DB=2 32 | REDIS_KEY_PREFIX="funpi_demo" 33 | # JWT 配置 34 | JWT_SECRET="funpi123456" 35 | JWT_EXPIRES_IN="30d" 36 | JWT_ALGORITHM="HS256" 37 | # 邮箱配置 38 | MAIL_HOST='demo.com' 39 | MAIL_PORT=465 40 | MAIL_POOL=1 41 | MAIL_SECURE=1 42 | MAIL_USER='demo@qq.com' 43 | MAIL_PASS='' 44 | MAIL_SENDER='易接口' 45 | MAIL_ADDRESS='demo@qq.com' 46 | -------------------------------------------------------------------------------- /packages/api/.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV="production" 2 | # 项目名称 3 | APP_NAME="易接口" 4 | APP_PORT=3000 5 | # MD5加密盐 6 | MD5_SALT="funpi123456" 7 | # 监听端口 8 | LISTEN_HOST="127.0.0.1" 9 | # 开发管理员密码 10 | DEV_PASSWORD="funpi123456" 11 | # 请求体大小 12 | BODY_LIMIT=10 13 | # 参数检查 14 | PARAMS_CHECK=0 15 | # 日志等级 16 | LOG_LEVEL="warn" 17 | # 时区 18 | TIMEZONE="Asia/Shanghai" 19 | # mysql 配置 20 | MYSQL_HOST="127.0.0.1" 21 | MYSQL_PORT=3306 22 | MYSQL_DB="funpi_demo" 23 | MYSQL_USERNAME="root" 24 | MYSQL_PASSWORD="root" 25 | TABLE_PRIMARY_KEY="default" 26 | # redis 配置 27 | REDIS_HOST="127.0.0.1" 28 | REDIS_PORT=6379 29 | REDIS_USERNAME="" 30 | REDIS_PASSWORD="" 31 | REDIS_DB=0 32 | REDIS_KEY_PREFIX="funpi_demo" 33 | # JWT 配置 34 | JWT_SECRET="funpi123456" 35 | JWT_EXPIRES_IN="30d" 36 | JWT_ALGORITHM="HS256" 37 | # 邮箱配置 38 | MAIL_HOST='demo.com' 39 | MAIL_PORT=465 40 | MAIL_POOL=1 41 | MAIL_SECURE=1 42 | MAIL_USER='demo@qq.com' 43 | MAIL_PASS='' 44 | MAIL_SENDER='易接口' 45 | MAIL_ADDRESS='demo@qq.com' 46 | -------------------------------------------------------------------------------- /packages/api/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | esdata 6 | data 7 | .cache 8 | lab 9 | labs 10 | cache 11 | .VSCodeCounter 12 | oclif.manifest.json 13 | dist 14 | report.html 15 | dist-ssr 16 | *.local 17 | .vscode-test/ 18 | *.vsix 19 | out 20 | CHANGELOG.md 21 | note.md 22 | .changeset 23 | addons2 24 | 25 | # Editor directories and files 26 | .vscode/* 27 | .cache 28 | .yicode/auto-imports.d.ts 29 | !.vscode/extensions.json 30 | .idea 31 | .DS_Store 32 | *.suo 33 | *.ntvs* 34 | *.njsproj 35 | *.sln 36 | *.sw? 37 | 38 | # Runtime data 39 | pids 40 | *.pid 41 | *.seed 42 | 43 | # Directory for instrumented libs generated by jscoverage/JSCover 44 | lib-cov 45 | 46 | # Coverage directory used by tools like istanbul 47 | coverage 48 | 49 | # nyc test coverage 50 | .nyc_output 51 | 52 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 53 | .grunt 54 | 55 | # node-waf configuration 56 | .lock-wscript 57 | 58 | # Compiled binary addons (http://nodejs.org/api/addons.html) 59 | build/Release 60 | 61 | # Dependency directories 62 | node_modules 63 | jspm_packages 64 | 65 | # Optional npm cache directory 66 | .npm 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # 0x 72 | profile-* 73 | 74 | # mac files 75 | .DS_Store 76 | 77 | # vim swap files 78 | *.swp 79 | 80 | # webstorm 81 | .idea 82 | 83 | # vscode 84 | .vscode 85 | *code-workspace 86 | 87 | # clinic 88 | profile* 89 | *clinic* 90 | *flamegraph* 91 | -------------------------------------------------------------------------------- /packages/api/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com 2 | # registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /packages/api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 1024, 7 | "bracketSpacing": true, 8 | "useTabs": false, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /packages/api/README.md: -------------------------------------------------------------------------------- 1 | # funpi 是什么? 2 | 3 | 中文名称 `放屁` 接口框架。 4 | 5 | 像放屁一样简单又自然的 `Node.js` 接口开发框架。 6 | 7 | > 注意:本项目 `v6` 版本为自用,`v7` 为内测版,`v8` 才是公测版。 8 | 9 | ### 仓库地址 10 | 11 | [github - https://github.com/chenbimo/funpi](https://github.com/chenbimo/funpi) 12 | 13 | ### 作者介绍 14 | 15 | [前端之虎陈随易 https://chensuiyi.me](https://chensuiyi.me) 16 | 17 | ### 文档教程 18 | 19 | 请到 [前端之虎陈随易 https://chensuiyi.me](https://chensuiyi.me) 网站查看。 20 | -------------------------------------------------------------------------------- /packages/api/addons/weixin/apis/pay/_meta.js: -------------------------------------------------------------------------------- 1 | export const metaConfig = { 2 | // 目录名称 3 | dirName: '管理员', 4 | // 接口名称 5 | apiNames: { 6 | select: '查询管理员接口权限', 7 | getMenus: '查询管理员菜单权限', 8 | adminLogin: '管理员登录', 9 | adminLoginLogSelectPage: '管理员登录日志-分页', 10 | adminActionLogSelectPage: '管理员操作日志-分页', 11 | adminInsert: '添加管理员', 12 | adminDelete: '删除管理员', 13 | adminSelectPage: '查询管理员-分页', 14 | adminUpdate: '更新管理员', 15 | apiSelectAll: '查询接口-全部', 16 | apiSelectPage: '查询接口-分页', 17 | menuDelete: '删除菜单', 18 | menuSelectAll: '查询菜单-全部', 19 | menuSelectPage: '查询菜单-分页', 20 | menuUpdate: '更新菜单', 21 | menuInsert: '添加菜单', 22 | roleDelete: '删除角色', 23 | roleInsert: '添加角色', 24 | roleSelectAll: '查询角色-全部', 25 | roleSelectPage: '查询角色-分页', 26 | roleUpdate: '更新角色', 27 | mailSelectPage: '查询邮件日志-分页' 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /packages/api/addons/weixin/apis/pay/select.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnField, httpConfig } from 'funpi'; 2 | 3 | export default async (fastify) => { 4 | fnRoute(import.meta.url, fastify, { 5 | apiName: '查询案例', 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | page: fnSchema('page'), 11 | limit: fnSchema('limit') 12 | }, 13 | required: [] 14 | }, 15 | 16 | // 执行函数 17 | apiHandler: async (req) => { 18 | try { 19 | const newsModel = fastify.mysql // 20 | .table('news'); 21 | 22 | // 记录总数 23 | const { totalCount } = await newsModel 24 | .clone() // 25 | .selectCount(); 26 | 27 | // 记录列表 28 | const rows = await newsModel 29 | .clone() // 30 | .orderBy('created_at', 'desc') 31 | .selectData(req.body.page, req.body.limit); 32 | 33 | return { 34 | ...httpConfig.SELECT_SUCCESS, 35 | data: { 36 | total: totalCount, 37 | rows: rows, 38 | page: req.body.page, 39 | limit: req.body.limit 40 | } 41 | }; 42 | } catch (err) { 43 | fastify.log.error(err); 44 | return httpConfig.SELECT_FAIL; 45 | } 46 | } 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/api/addons/weixin/tables/pay.js: -------------------------------------------------------------------------------- 1 | export const tableName = '微信插件表'; 2 | export const tableData = { 3 | title: { 4 | name: '新闻标题', 5 | type: 'mediumText', 6 | min: 1, 7 | max: 50 8 | }, 9 | content: { 10 | name: '新闻内容', 11 | type: 'mediumText', 12 | min: 0 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/api/apis/example/_ffffff.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnField, httpConfig } from 'funpi'; 2 | import { tableData } from '../../tables/example.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | apiName: '查询案例', 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | page: fnSchema('page'), 12 | limit: fnSchema('limit') 13 | }, 14 | required: [] 15 | }, 16 | 17 | // 执行函数 18 | apiHandler: async (req) => { 19 | try { 20 | const newsModel = fastify.mysql // 21 | .table('news'); 22 | 23 | // 记录总数 24 | const { totalCount } = await newsModel 25 | .clone() // 26 | .selectCount(); 27 | 28 | // 记录列表 29 | const rows = await newsModel 30 | .clone() // 31 | .orderBy('created_at', 'desc') 32 | .selectData(req.body.page, req.body.limit, fnField(tableData)); 33 | 34 | return { 35 | ...httpConfig.SELECT_SUCCESS, 36 | data: { 37 | total: totalCount, 38 | rows: rows, 39 | page: req.body.page, 40 | limit: req.body.limit 41 | } 42 | }; 43 | } catch (err) { 44 | fastify.log.error(err); 45 | return httpConfig.SELECT_FAIL; 46 | } 47 | } 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/api/apis/example/_meta.js: -------------------------------------------------------------------------------- 1 | export const metaConfig = { 2 | // 目录名称 3 | dirName: '演示表', 4 | // 接口名称 5 | apiNames: { 6 | insert: '添加功能演示', 7 | delete: '删除功能演示', 8 | select: '分页功能演示', 9 | update: '更新功能演示', 10 | detail: '详情功能演示' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /packages/api/apis/example/delete.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, httpConfig } from 'funpi'; 2 | 3 | export default async (fastify) => { 4 | fnRoute(import.meta.url, fastify, { 5 | apiName: '删除案例', 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | id: fnSchema('id') 11 | }, 12 | required: ['id'] 13 | }, 14 | // 执行函数 15 | apiHandler: async (req) => { 16 | try { 17 | const newsModel = fastify.mysql // 18 | .table('news'); 19 | 20 | const result = await newsModel.clone().where('id', req.body.id).deleteData(); 21 | 22 | return { 23 | ...httpConfig.INSERT_SUCCESS, 24 | data: result 25 | }; 26 | } catch (err) { 27 | fastify.log.error(err); 28 | return httpConfig.SELECT_FAIL; 29 | } 30 | } 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/api/apis/example/detail.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnField, httpConfig } from 'funpi'; 2 | import { tableData } from '../../tables/example.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | apiName: '查询案例详情', 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | id: fnSchema('id') 12 | }, 13 | required: ['id'] 14 | }, 15 | 16 | // 执行函数 17 | apiHandler: async (req) => { 18 | try { 19 | const newsModel = fastify.mysql.table('news'); 20 | 21 | const result = await newsModel // 22 | .clone() 23 | .where({ id: req.body.id }) 24 | .selectOne(fnField(tableData)); 25 | 26 | return { 27 | ...httpConfig.SELECT_SUCCESS, 28 | data: result 29 | }; 30 | } catch (err) { 31 | fastify.log.error(err); 32 | return httpConfig.SELECT_FAIL; 33 | } 34 | } 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/api/apis/example/insert.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, httpConfig } from 'funpi'; 2 | import { tableData } from '../../tables/example.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | apiName: '添加案例', 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | title: fnSchema(tableData.title), 12 | content: fnSchema(tableData.content) 13 | }, 14 | required: ['title', 'content'] 15 | }, 16 | 17 | // 执行函数 18 | apiHandler: async (req) => { 19 | const trx = await fastify.mysql.transaction(); 20 | try { 21 | const newsModel = trx('example'); 22 | 23 | const result = await newsModel // 24 | .clone() 25 | .insertData({ 26 | title: req.body.title, 27 | content: req.body.content 28 | }); 29 | 30 | await trx.commit(); 31 | return { 32 | ...httpConfig.INSERT_SUCCESS, 33 | data: result 34 | }; 35 | } catch (err) { 36 | await trx.rollback(); 37 | fastify.log.error(err); 38 | return httpConfig.INSERT_FAIL; 39 | } 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/api/apis/example/select.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnField, httpConfig } from 'funpi'; 2 | import { tableData } from '../../tables/example.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | apiName: '查询案例', 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | page: fnSchema('page'), 12 | limit: fnSchema('limit') 13 | }, 14 | required: [] 15 | }, 16 | 17 | // 执行函数 18 | apiHandler: async (req) => { 19 | try { 20 | const newsModel = fastify.mysql // 21 | .table('news'); 22 | 23 | // 记录总数 24 | const { totalCount } = await newsModel 25 | .clone() // 26 | .selectCount(); 27 | 28 | // 记录列表 29 | const rows = await newsModel 30 | .clone() // 31 | .orderBy('created_at', 'desc') 32 | .selectData(req.body.page, req.body.limit, fnField(tableData)); 33 | 34 | return { 35 | ...httpConfig.SELECT_SUCCESS, 36 | data: { 37 | total: totalCount, 38 | rows: rows, 39 | page: req.body.page, 40 | limit: req.body.limit 41 | } 42 | }; 43 | } catch (err) { 44 | fastify.log.error(err); 45 | return httpConfig.SELECT_FAIL; 46 | } 47 | } 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/api/apis/example/update.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, httpConfig } from 'funpi'; 2 | import { tableData } from '../../tables/example.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | apiName: '更新案例', 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | id: fnSchema('id'), 12 | title: fnSchema(tableData.title), 13 | content: fnSchema(tableData.content) 14 | }, 15 | required: ['id'] 16 | }, 17 | 18 | // 执行函数 19 | apiHandler: async (req) => { 20 | try { 21 | const newsModel = fastify.mysql.table('news'); 22 | 23 | const result = await newsModel // 24 | .clone() 25 | .where('id', req.body.id) 26 | .updateData({ 27 | title: req.body.title, 28 | content: req.body.content 29 | }); 30 | 31 | return { 32 | ...httpConfig.INSERT_SUCCESS, 33 | data: result 34 | }; 35 | } catch (err) { 36 | fastify.log.error(err); 37 | return httpConfig.INSERT_FAIL; 38 | } 39 | } 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/api/config/custom.js: -------------------------------------------------------------------------------- 1 | // 自定义配置 2 | const customConfig = { 3 | tencentCloud: { 4 | secretId: '', 5 | secretKey: '', 6 | bucket: '', 7 | region: '' 8 | } 9 | }; 10 | 11 | export { customConfig }; 12 | -------------------------------------------------------------------------------- /packages/api/config/menu.js: -------------------------------------------------------------------------------- 1 | export const menuConfig = [ 2 | // 以下为菜单格式示例 3 | // { 4 | // path: '/banner', 5 | // name: '轮播管理', 6 | // children: [ 7 | // { 8 | // path: '/banner/lists', 9 | // name: '轮播列表' 10 | // } 11 | // ] 12 | // } 13 | ]; 14 | -------------------------------------------------------------------------------- /packages/api/funpi.js: -------------------------------------------------------------------------------- 1 | import { initServer } from 'funpi'; 2 | initServer(); 3 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@funpi/api", 3 | "version": "7.20.27", 4 | "description": "FunPi(放屁) - 接口端", 5 | "main": "funpi.js", 6 | "type": "module", 7 | "private": false, 8 | "publishConfig": { 9 | "access": "public", 10 | "registry": "https://registry.npmjs.org" 11 | }, 12 | "scripts": { 13 | "dev": "bun --watch --env-file=./.env.development funpi.js", 14 | "server": "pm2 start pm2.config.cjs", 15 | "ct": "bun ./scripts/checkTable.js", 16 | "syncDb:dev": "bun --env-file=./.env.development ./scripts/syncMysql.js", 17 | "syncDb:prod": "bun --env-file=./.env.production ./scripts/syncMysql.js" 18 | }, 19 | "keywords": [ 20 | "api", 21 | "nodejs", 22 | "fastify" 23 | ], 24 | "files": [ 25 | "apis/", 26 | "config/", 27 | "plugins/", 28 | "public/", 29 | "scripts/", 30 | "tables/", 31 | ".env.development", 32 | ".env.production", 33 | ".gitignore", 34 | ".npmrc", 35 | ".prettier", 36 | "funpi.js", 37 | "LICENSE", 38 | "package.json", 39 | "pm2.config.cjs", 40 | "README.md" 41 | ], 42 | "author": "chensuiyi ", 43 | "homepage": "https://chensuiyi.me", 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/chenbimo/yicode.git" 47 | }, 48 | "dependencies": { 49 | "dotenv": "^16.5.0", 50 | "funpi": "workspace:^" 51 | }, 52 | "gitHead": "1c28c0de7c0af8aa4582c45ab2d98e66c597c7a1" 53 | } 54 | -------------------------------------------------------------------------------- /packages/api/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenbimo/funpi/d81b828cb65d51c31cbc6200c283b2d81caf8fd6/packages/api/plugins/.gitkeep -------------------------------------------------------------------------------- /packages/api/pm2.config.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const dotenv = require('dotenv'); 3 | const envConfig = dotenv.parse(fs.readFileSync('./.env.production')); 4 | 5 | module.exports = { 6 | apps: [ 7 | { 8 | name: 'funpi', 9 | instances: 1, 10 | script: './funpi.js', 11 | exec_mode: 'cluster', 12 | watch: false, 13 | autorestart: true, 14 | interpreter: 'bun', 15 | ignore_watch: ['node_modules', 'logs', 'data'], 16 | env: envConfig, 17 | log_file: './logs/funpi.log', 18 | error_file: './logs/funpi-error.log', 19 | out_file: './logs/funpi-out.log', 20 | max_memory_restart: '200M' 21 | } 22 | ] 23 | }; 24 | -------------------------------------------------------------------------------- /packages/api/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenbimo/funpi/d81b828cb65d51c31cbc6200c283b2d81caf8fd6/packages/api/public/.gitkeep -------------------------------------------------------------------------------- /packages/api/scripts/checkTable.js: -------------------------------------------------------------------------------- 1 | import { checkTable } from 'funpi/scripts/checkTable'; 2 | checkTable(); 3 | -------------------------------------------------------------------------------- /packages/api/scripts/syncMysql.js: -------------------------------------------------------------------------------- 1 | import { syncMysql } from 'funpi/scripts/syncMysql'; 2 | syncMysql(); 3 | -------------------------------------------------------------------------------- /packages/api/tables/example.js: -------------------------------------------------------------------------------- 1 | export const tableName = '新闻示例表'; 2 | export const tableData = { 3 | title: { 4 | name: '新闻标题', 5 | type: 'mediumText', 6 | min: 1, 7 | max: 50 8 | }, 9 | content: { 10 | name: '新闻内容', 11 | type: 'mediumText', 12 | min: 0 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # funpi 是什么? 2 | 3 | 中文名称 `放屁` 接口框架。 4 | 5 | 像放屁一样简单又自然的 `Node.js` 接口开发框架。 6 | 7 | > 注意:本项目 `v6` 版本为自用,`v7` 为内测版,`v8` 才是公测版。 8 | 9 | ### 仓库地址 10 | 11 | [github - https://github.com/chenbimo/funpi](https://github.com/chenbimo/funpi) 12 | 13 | ### 作者介绍 14 | 15 | [前端之虎陈随易 https://chensuiyi.me](https://chensuiyi.me) 16 | 17 | ### 文档教程 18 | 19 | 请到 [前端之虎陈随易 https://chensuiyi.me](https://chensuiyi.me) 网站查看。 20 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@funpi/cli", 3 | "version": "7.20.27", 4 | "description": "FunPi(放屁) - 命令行占位", 5 | "type": "module", 6 | "private": false, 7 | "publishConfig": { 8 | "access": "public", 9 | "registry": "https://registry.npmjs.org" 10 | }, 11 | "exports": {}, 12 | "keywords": [ 13 | "fastify", 14 | "nodejs", 15 | "api" 16 | ], 17 | "files": [ 18 | "LICENSE", 19 | "package.json", 20 | "README.md" 21 | ], 22 | "engines": { 23 | "node": ">=20.6.0" 24 | }, 25 | "author": "chensuiyi ", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/chenbimo/funpi" 29 | }, 30 | "homepage": "https://chensuiyi.me", 31 | "gitHead": "1c28c0de7c0af8aa4582c45ab2d98e66c597c7a1" 32 | } 33 | -------------------------------------------------------------------------------- /packages/funpi/README.md: -------------------------------------------------------------------------------- 1 | # funpi 是什么? 2 | 3 | 中文名称 `放屁` 接口框架。 4 | 5 | 像放屁一样简单又自然的 `Node.js` 接口开发框架。 6 | 7 | > 注意:本项目 `v7` 为内测版,`v8` 才是公测版,谨慎使用。 8 | > 9 | > 自 `v7.15.0` 版本开始,本项目仅支持 [Bun](https://bun.sh),不再支持 `Node.js`。 10 | 11 | ### 仓库地址 12 | 13 | [github - https://github.com/chenbimo/funpi](https://github.com/chenbimo/funpi) 14 | 15 | ### 使用教程 16 | 17 | [funpi(放屁)使用文档](https://sourl.cn/bUq25t) 18 | 19 | ### 作者介绍 20 | 21 | [前端之虎陈随易 https://chensuiyi.me](https://chensuiyi.me) 22 | 23 | ### 演示地址 24 | 25 | - [https://funpi-demo.yicode.tech](https://funpi-demo.yicode.tech) 26 | 27 | ### 功能特点 28 | 29 | - ✅ 只需 `简单配置`,即可快速上手开发。 30 | - ✅ 自动生成 `接口文档`,方便前后端对接。 31 | - ✅ 自带 `权限`、`角色`、`管理`、`日志`、`菜单`、`接口`、`字典` 等基础功能。 32 | - ✅ 自带 `邮件发送`,`文件上传` 等功能。 33 | - ✅ 自带 `日志打印` 和 `日志分割` 功能。 34 | - ✅ 自带 `jwt` 鉴权机制。 35 | - ✅ 自带 `登录日志`,`邮件日志` 等功能。 36 | - ✅ 自带配套的后台管理系统 `yiadmin`,30 分钟搭建一个后台管理系统。 37 | - ✅ 默认已处理 `跨域` 问题,无需再为跨域担心。 38 | - ✅ 优先使用 `缓存`,提高应用性能。 39 | - ✅ 默认提供 `静态文件托管` 功能。 40 | - ✅ 可以 `一键更新` 后台管理系统。 41 | - ✅ 全面的 `接口参数验证` 功能,极大减少安全隐患。 42 | - ✅ 提供 `数据库表字段设计` 和 `表结构同步` 功能。 43 | 44 | ### 功能限制 45 | 46 | 本框架做了很多约束,减少自由度,增加确定度,稳定度。 47 | 48 | - ❎ 仅支持 `Bun`,不支持 `Node.js`,`Deno` 等。 49 | - ❎ 仅支持 `单机部署`,使用 `pm2` 管理。 50 | - ❎ 仅支持 `单角色权限`。 51 | - ❎ 仅支持 `Mysql` 关系数据库。 52 | - ❎ 仅支持 `Redis` 缓存数据库。 53 | - ❎ 仅支持 `POST` 和 `GET` 请求方法。 54 | - ❎ 仅支持 `整数`、`浮点数`、`文本`、`字符串` 这四种数据库字段类型。 55 | - ❎ 不支持 `分库分表`。 56 | - ❎ 不支持 `Docker` 部署,请自行研究。 57 | - ❎ 不支持 `分布式部署`。 58 | - ❎ 不支持 `Restful` 规范,不认同 `Restful` 规范,不使用 `Restful` 规范。 59 | 60 | ### 付费插件 61 | 62 | - `微信扫码插件`,登录注册,需要提供微信公众号。 63 | - `在线人数统计插件`,提供 `踢人`,`拉黑` 等功能。 64 | - `微信支付插件`,支持 `多产品`、`折扣`、`优惠` 等功能。 65 | 66 | ### 注意事项 67 | 68 | - 与本项目逻辑、BUG、建议相关的问题,请联系作者无偿 `免费处理`。 69 | - 与本项目无关的业务、功能、需求、部署相关的问题,请联系作者 `有偿咨询`。 70 | 71 | ### 实际效果 72 | 73 | 使用 `funpi` + `yiadmin` 驱动的,免费且开源的后台管理系统。 74 | 75 | #### 📄 登录页面 76 | 77 | ![picture 0](https://static.yicode.tech/images/202311/20231126000719.png) 78 | 79 | #### 📄 菜单页面 80 | 81 | ![picture 2](https://static.yicode.tech/images/202311/20231126000809.png) 82 | 83 | #### 📄 接口页面 84 | 85 | ![picture 3](https://static.yicode.tech/images/202311/20231126000833.png) 86 | 87 | #### 📄 角色页面 88 | 89 | ![picture 4](https://static.yicode.tech/images/202311/20231126000913.png) 90 | 91 | #### 📄 登录日志 92 | 93 | ![picture 5](https://static.yicode.tech/images/202311/20231126000935.png) 94 | 95 | #### 📄 邮件日志 96 | 97 | ![picture 6](https://static.yicode.tech/images/202311/20231126001012.png) 98 | 99 | ### 版权说明 100 | 101 | `funpi(放屁)` 使用 `Apache 2.0` 协议开源 102 | 103 | > 一句话总结:开源不等于放弃版权,不可侵犯原作者版权,改动处要做说明,可以闭源使用。 104 | 105 | 拥有版权(Copyright)意味着你对你开发的软件及其源代码拥有著作权,所有权和其他法定权利,使用一个开源协议并不意味着放弃版权。 106 | 107 | 在 `Apache 2.0` 协议许可下,您可以: 108 | 109 | - **商业化使用**(这意味着,您可以出于商业目的使用这些源代码) 110 | - **再分发**(这意味着,您可以将源代码副本传输给其他任何人) 111 | - **修改**(这意味着,您可以修改源代码) 112 | - **专利使用**(这意味着,版权人明确声明授予您专利使用权) 113 | - **私人使用**(这意味着,您可以出于一切目的私下使用和修改源代码) 114 | 115 | 唯须遵守以下条款: 116 | 117 | - **协议和版权通知**(这意味着,软件中必须包含许可证和版权声明的副本) 118 | - **状态更改说明**(如果您更改软件,您应当提供适当的说明) 119 | 120 | 除此之外,该软件: 121 | 122 | - **提供责任限制**(版权人声明不对使用者造成的任何损失负责) 123 | - **限制商标使用** (不能使用版权人的商标) 124 | - **不提供任何担保**(版权人声明不为该软件的品质提供任何担保) 125 | 126 | 进一步说明: 127 | 128 | 1. 本软件又叫本 **作品**,可以是源码,也可以是编译或转换后的其他形式。**衍生作品** 是在本作品的基础上修改后的有原创性的工作成果。本作品的 **贡献者** 包括许可人和其他提交了贡献的人,以下统称 **我**。 129 | 2. 我授予你权利:你可以免费复制、使用、修改、再许可、分发本作品及衍生作品(可以不用公开源码)。 130 | 3. 如果本软件涉及我的专利(或潜在专利),我在此授予你专利许可,你可以永久性地免费使用此专利,用于制作、使用、出售、转让本作品。如果你哪天居然告本作品侵权,你的专利许可在你告我那天被收回。 131 | 4. 你在复制和分发本作品或衍生作品时,要满足以下条件。 132 | 133 | - 带一份本许可证。 134 | - 如果你修改了什么,要在改动的文件中有明显的修改声明。 135 | - 如果你以源码形式分发,你必须保留本作品的版权、专利、商标和归属声明。 136 | - 如果本作品带了 **NOTICE** 文件,你就得带上 **NOTICE** 文件中包含的归属声明。即便你的发布是不带源码的,你也得带上此文件,并在作品某处予以展示。 137 | - 你可以对自己的修改添加版权说明。对于你的修改或者整个衍生作品,你可以使用不同的许可,但你对本作品的使用、复制和分发等,必须符合本许可证规定。 138 | 139 | 5. 你提交贡献就表明你默认遵守本许可的条款和条件。当然,你可以和我签订另外的专门的条款。 140 | 6. 你不许使用我的商品名、商标、服务标志或产品名。 141 | 7. 本作品是 **按原样**(AS IS)提供的,没有任何保证啊,你懂的。 142 | 8. 我可不负任何责任。除非我书面同意,或者法律有这样的要求(例如对故意和重大过失行为负责)。 143 | 9. 你可以向别人提供保证,你可以向别人收费,但那都是你的事,别给我惹麻烦。 144 | 145 | 注意以上的 **我**,既包含了许可人,也包含了每位 **贡献者**。 146 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/_meta.js: -------------------------------------------------------------------------------- 1 | export const metaConfig = { 2 | // 目录名称 3 | dirName: '管理员', 4 | // 接口名称 5 | apiNames: { 6 | getApis: '查询管理员接口权限', 7 | getMenus: '查询管理员菜单权限', 8 | adminLogin: '管理员登录', 9 | adminLoginLogSelectPage: '管理员登录日志-分页', 10 | adminActionLogSelectPage: '管理员操作日志-分页', 11 | adminInsert: '添加管理员', 12 | adminDelete: '删除管理员', 13 | adminSelectPage: '查询管理员-分页', 14 | adminUpdate: '更新管理员', 15 | apiSelectAll: '查询接口-全部', 16 | apiSelectPage: '查询接口-分页', 17 | menuDelete: '删除菜单', 18 | menuSelectAll: '查询菜单-全部', 19 | menuSelectPage: '查询菜单-分页', 20 | menuUpdate: '更新菜单', 21 | menuInsert: '添加菜单', 22 | roleDelete: '删除角色', 23 | roleInsert: '添加角色', 24 | roleSelectAll: '查询角色-全部', 25 | roleSelectPage: '查询角色-分页', 26 | roleUpdate: '更新角色', 27 | roleDetail: '角色详情', 28 | mailSelectPage: '查询邮件日志-分页', 29 | apiUpdateState: '更新接口状态' 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/adminActionLogSelectPage.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnField } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | import { tableData } from '../../tables/adminActionLog.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | page: fnSchema('page'), 12 | limit: fnSchema('limit') 13 | } 14 | }, 15 | // 执行函数 16 | apiHandler: async (req) => { 17 | try { 18 | const adminActionLogModel = fastify.mysql // 19 | .table('sys_admin_action_log'); 20 | 21 | const { totalCount } = await adminActionLogModel.clone().selectCount(); 22 | const rows = await adminActionLogModel 23 | // 24 | .clone() 25 | .orderBy('created_at', 'desc') 26 | .selectData(req.body.page, req.body.limit, fnField(tableData)); 27 | 28 | return { 29 | ...httpConfig.SELECT_SUCCESS, 30 | data: { 31 | total: totalCount, 32 | rows: rows, 33 | page: req.body.page, 34 | limit: req.body.limit 35 | } 36 | }; 37 | } catch (err) { 38 | fastify.log.error(err); 39 | return httpConfig.SELECT_FAIL; 40 | } 41 | } 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/adminDelete.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | id: fnSchema('id') 11 | }, 12 | required: ['id'] 13 | }, 14 | // 执行函数 15 | apiHandler: async (req) => { 16 | try { 17 | const adminModel = fastify.mysql.table('sys_admin').where({ id: req.body.id }); 18 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 19 | 20 | const adminData = await adminModel.clone().selectOne(['id']); 21 | if (!adminData?.id) { 22 | return httpConfig.NO_DATA; 23 | } 24 | 25 | const result = await adminModel.deleteData(); 26 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req))); 27 | 28 | return { 29 | ...httpConfig.DELETE_SUCCESS, 30 | data: result 31 | }; 32 | } catch (err) { 33 | fastify.log.error(err); 34 | return httpConfig.DELETE_FAIL; 35 | } 36 | } 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/adminInsert.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog, fnCryptoMD5, fnCryptoHmacMD5 } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | import { tableData } from '../../tables/admin.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | username: fnSchema(tableData.username), 12 | password: fnSchema(tableData.password), 13 | nickname: fnSchema(tableData.nickname), 14 | role: fnSchema(tableData.role) 15 | }, 16 | required: ['username', 'password', 'nickname', 'role'] 17 | }, 18 | // 执行函数 19 | apiHandler: async (req) => { 20 | try { 21 | if (req.body.role === 'dev') { 22 | return { 23 | ...httpConfig.FAIL, 24 | msg: '不能增加开发管理员角色' 25 | }; 26 | } 27 | const adminModel = fastify.mysql.table('sys_admin'); 28 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 29 | 30 | const adminData = await adminModel.clone().where('username', req.body.username).selectOne(['id']); 31 | if (adminData?.id) { 32 | return { 33 | ...httpConfig.FAIL, 34 | msg: '管理员账号已存在' 35 | }; 36 | } 37 | 38 | const result = await adminModel.clone().insertData({ 39 | username: req.body.username, 40 | password: fnCryptoHmacMD5(fnCryptoMD5(req.body.password), process.env.MD5_SALT), 41 | nickname: req.body.nickname, 42 | role: req.body.role 43 | }); 44 | 45 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req, ['password']))); 46 | 47 | return { 48 | ...httpConfig.INSERT_SUCCESS, 49 | data: result 50 | }; 51 | } catch (err) { 52 | fastify.log.error(err); 53 | return httpConfig.INSERT_FAIL; 54 | } 55 | } 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/adminLogin.js: -------------------------------------------------------------------------------- 1 | import { omit as es_omit } from 'es-toolkit'; 2 | import { fnRoute, fnSchema, fnCryptoHmacMD5 } from '../../utils/index.js'; 3 | import { httpConfig } from '../../config/http.js'; 4 | import { tableData } from '../../tables/admin.js'; 5 | 6 | export default async (fastify) => { 7 | fnRoute(import.meta.url, fastify, { 8 | // 请求参数约束 9 | schemaRequest: { 10 | type: 'object', 11 | properties: { 12 | account: fnSchema({ name: '账号', type: 'string', min: 1, max: 100 }), 13 | password: fnSchema(tableData.password) 14 | }, 15 | required: ['account', 'password'] 16 | }, 17 | // 执行函数 18 | apiHandler: async (req) => { 19 | try { 20 | const adminModel = fastify.mysql.table('sys_admin'); 21 | const adminLoginLogModel = fastify.mysql.table('sys_admin_login_log'); 22 | 23 | // 查询管理员是否存在 24 | // TODO: 增加邮箱注册和邮箱登录 25 | const adminData = await adminModel // 26 | .clone() 27 | .orWhere({ username: req.body.account }) 28 | .selectOne(['id', 'password', 'username', 'nickname', 'role']); 29 | 30 | // 判断用户存在 31 | if (!adminData?.id) { 32 | return { 33 | ...httpConfig.FAIL, 34 | msg: '用户不存在' 35 | }; 36 | } 37 | 38 | // 判断密码 39 | if (fnCryptoHmacMD5(req.body.password, process.env.MD5_SALT) !== adminData.password) { 40 | return { 41 | ...httpConfig.FAIL, 42 | msg: '密码错误' 43 | }; 44 | } 45 | // 记录登录日志 46 | await adminLoginLogModel.clone().insertData({ 47 | user_id: adminData.id, 48 | username: adminData.username, 49 | nickname: adminData.nickname, 50 | role: adminData.role, 51 | ip: req.ip || '', 52 | ua: req.headers['user-agent'] || '' 53 | }); 54 | 55 | // 成功返回 56 | return { 57 | ...httpConfig.SUCCESS, 58 | msg: '登录成功', 59 | data: es_omit(adminData, ['password']), 60 | token: await fastify.jwt.sign({ 61 | id: adminData.id, 62 | username: adminData.username, 63 | nickname: adminData.nickname, 64 | role_type: 'admin', 65 | role: adminData.role 66 | }) 67 | }; 68 | } catch (err) { 69 | fastify.log.error(err); 70 | return { 71 | ...httpConfig.FAIL, 72 | msg: '登录失败' 73 | }; 74 | } 75 | } 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/adminLoginLogSelectPage.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnField } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | import { tableData } from '../../tables/adminLoginLog.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | page: fnSchema('page'), 12 | limit: fnSchema('limit') 13 | } 14 | }, 15 | // 执行函数 16 | apiHandler: async (req) => { 17 | try { 18 | const adminLoginLogModel = fastify.mysql // 19 | .table('sys_admin_login_log'); 20 | 21 | const { totalCount } = await adminLoginLogModel.clone().selectCount(); 22 | const rows = await adminLoginLogModel 23 | // 24 | .clone() 25 | .orderBy('created_at', 'desc') 26 | .selectData(req.body.page, req.body.limit, fnField(tableData)); 27 | 28 | return { 29 | ...httpConfig.SELECT_SUCCESS, 30 | data: { 31 | total: totalCount, 32 | rows: rows, 33 | page: req.body.page, 34 | limit: req.body.limit 35 | } 36 | }; 37 | } catch (err) { 38 | fastify.log.error(err); 39 | return httpConfig.SELECT_FAIL; 40 | } 41 | } 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/adminSelectPage.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnField } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | import { tableData } from '../../tables/admin.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | page: fnSchema('page'), 12 | limit: fnSchema('limit') 13 | } 14 | }, 15 | // 执行函数 16 | apiHandler: async (req) => { 17 | try { 18 | const adminModel = fastify.mysql // 19 | .table('sys_admin') 20 | .where('username', '<>', 'dev'); 21 | 22 | const { totalCount } = await adminModel.clone().selectCount(); 23 | const rows = await adminModel 24 | // 25 | .clone() 26 | .orderBy('created_at', 'desc') 27 | .selectData(req.body.page, req.body.limit, fnField(tableData, ['password'])); 28 | 29 | return { 30 | ...httpConfig.SELECT_SUCCESS, 31 | data: { 32 | total: totalCount, 33 | rows: rows, 34 | page: req.body.page, 35 | limit: req.body.limit 36 | } 37 | }; 38 | } catch (err) { 39 | fastify.log.error(err); 40 | return httpConfig.SELECT_FAIL; 41 | } 42 | } 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/adminUpdate.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog, fnCryptoHmacMD5 } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | import { tableData } from '../../tables/admin.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | id: fnSchema('id'), 12 | username: fnSchema(tableData.username), 13 | password: fnSchema(tableData.password), 14 | nickname: fnSchema(tableData.nickname), 15 | role: fnSchema(tableData.role) 16 | }, 17 | required: ['id'] 18 | }, 19 | // 执行函数 20 | apiHandler: async (req) => { 21 | try { 22 | if (req.body.role === 'dev') { 23 | return { 24 | ...httpConfig.FAIL, 25 | msg: '不能增加开发管理员角色' 26 | }; 27 | } 28 | const adminModel = fastify.mysql.table('sys_admin'); 29 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 30 | 31 | const adminData = await adminModel.clone().where('username', req.body.username).selectOne(['id']); 32 | if (adminData?.id && adminData?.id !== req.body.id) { 33 | return { 34 | ...httpConfig.FAIL, 35 | msg: '管理员账号已存在' 36 | }; 37 | } 38 | const updateData = { 39 | nickname: req.body.nickname, 40 | username: req.body.username, 41 | role: req.body.role 42 | }; 43 | 44 | if (req.body.password) { 45 | updateData.password = fnCryptoHmacMD5(req.body.password, process.env.MD5_SALT); 46 | } 47 | await adminModel.clone().where({ id: req.body.id }).updateData(updateData); 48 | 49 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req, ['password']))); 50 | 51 | return httpConfig.UPDATE_SUCCESS; 52 | } catch (err) { 53 | fastify.log.error(err); 54 | return httpConfig.UPDATE_FAIL; 55 | } 56 | } 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/apiSelectAll.js: -------------------------------------------------------------------------------- 1 | import { fnRoute } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: {} 10 | }, 11 | // 执行函数 12 | apiHandler: async () => { 13 | try { 14 | const result = await fastify.redisGet('cacheData:api'); 15 | 16 | return { 17 | ...httpConfig.SELECT_SUCCESS, 18 | data: { 19 | rows: result 20 | } 21 | }; 22 | } catch (err) { 23 | fastify.log.error(err); 24 | return httpConfig.SELECT_FAIL; 25 | } 26 | } 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/apiSelectPage.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | page: fnSchema('page'), 11 | limit: fnSchema('limit') 12 | } 13 | }, 14 | // 执行函数 15 | apiHandler: async (req) => { 16 | try { 17 | const apiModel = fastify.mysql.table('sys_api'); 18 | 19 | const { totalCount } = await apiModel.clone().selectCount(); 20 | 21 | const rows = await apiModel 22 | // 23 | .clone() 24 | .orderBy('created_at', 'desc') 25 | .selectData(req.body.page, req.body.limit); 26 | 27 | return { 28 | ...httpConfig.SELECT_SUCCESS, 29 | data: { 30 | total: totalCount, 31 | rows: rows, 32 | page: req.body.page, 33 | limit: req.body.limit 34 | } 35 | }; 36 | } catch (err) { 37 | fastify.log.error(err); 38 | return httpConfig.SELECT_FAIL; 39 | } 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/getApis.js: -------------------------------------------------------------------------------- 1 | import { fnRoute } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: {} 10 | }, 11 | // 执行函数 12 | apiHandler: async (req) => { 13 | try { 14 | const result = await fastify.getUserApis(req.session); 15 | return { 16 | ...httpConfig.SELECT_SUCCESS, 17 | data: { 18 | rows: result 19 | } 20 | }; 21 | } catch (err) { 22 | fastify.log.error(err); 23 | return httpConfig.SELECT_FAIL; 24 | } 25 | } 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/getMenus.js: -------------------------------------------------------------------------------- 1 | import { fnRoute } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: {} 10 | }, 11 | // 执行函数 12 | apiHandler: async (req) => { 13 | try { 14 | const result = await fastify.getUserMenus(req.session); 15 | return { 16 | ...httpConfig.SELECT_SUCCESS, 17 | data: { 18 | rows: result 19 | } 20 | }; 21 | } catch (err) { 22 | fastify.log.error(err); 23 | return httpConfig.SELECT_FAIL; 24 | } 25 | } 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/mailSelectPage.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnField } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | import { tableData } from '../../tables/mailLog.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | page: fnSchema('page'), 12 | limit: fnSchema('limit') 13 | }, 14 | required: [] 15 | }, 16 | // 执行函数 17 | apiHandler: async (req) => { 18 | try { 19 | const mailLogModel = fastify.mysql // 20 | .table('sys_mail_log') 21 | .modify(function (db) { 22 | if (req.body.keyword) { 23 | db.where('nickname', 'like', `%${req.body.keyword}%`); 24 | } 25 | }); 26 | 27 | // 记录总数 28 | const { totalCount } = await mailLogModel.clone().selectCount(); 29 | 30 | // 记录列表 31 | const rows = await mailLogModel 32 | // 33 | .clone() 34 | .orderBy('created_at', 'desc') 35 | .selectData(req.body.page, req.body.limit, fnField(tableData)); 36 | 37 | return { 38 | ...httpConfig.SELECT_SUCCESS, 39 | data: { 40 | total: totalCount, 41 | rows: rows, 42 | page: req.body.page, 43 | limit: req.body.limit 44 | } 45 | }; 46 | } catch (err) { 47 | fastify.log.error(err); 48 | return httpConfig.SELECT_FAIL; 49 | } 50 | } 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/menuSelectAll.js: -------------------------------------------------------------------------------- 1 | import { fnRoute } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: {} 10 | }, 11 | // 执行函数 12 | apiHandler: async () => { 13 | try { 14 | const result = await fastify.redisGet('cacheData:menu'); 15 | 16 | return { 17 | ...httpConfig.SELECT_SUCCESS, 18 | data: { 19 | rows: result 20 | } 21 | }; 22 | } catch (err) { 23 | fastify.log.error(err); 24 | return httpConfig.SELECT_FAIL; 25 | } 26 | } 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/menuSelectPage.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | page: fnSchema('page'), 11 | limit: fnSchema('limit') 12 | }, 13 | required: [] 14 | }, 15 | // 执行函数 16 | apiHandler: async (req) => { 17 | try { 18 | const menuModel = fastify.mysql // 19 | .table('sys_menu'); 20 | 21 | const { totalCount } = await menuModel.clone().selectCount(); 22 | 23 | const rows = await menuModel 24 | // 25 | .clone() 26 | .orderBy('created_at', 'desc') 27 | .selectData(req.body.page, req.body.limit); 28 | 29 | return { 30 | ...httpConfig.SELECT_SUCCESS, 31 | data: { 32 | total: totalCount, 33 | rows: rows, 34 | page: req.body.page, 35 | limit: req.body.limit 36 | } 37 | }; 38 | } catch (err) { 39 | fastify.log.error(err); 40 | return httpConfig.SELECT_FAIL; 41 | } 42 | } 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/roleDelete.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | id: fnSchema('id') 11 | }, 12 | required: ['id'] 13 | }, 14 | // 执行函数 15 | apiHandler: async (req) => { 16 | try { 17 | const roleModel = fastify.mysql.table('sys_role').where('id', req.body.id); 18 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 19 | 20 | const roleData = await roleModel.clone().selectOne(['id', 'is_system']); 21 | if (!roleData?.id) { 22 | return httpConfig.NO_DATA; 23 | } 24 | 25 | if (roleData.is_system === 1) { 26 | return { 27 | ...httpConfig.DELETE_FAIL, 28 | msg: '系统角色,无法删除' 29 | }; 30 | } 31 | 32 | const result = await roleModel.clone().deleteData(); 33 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req))); 34 | 35 | // 生成新的权限 36 | await fastify.cacheRoleData(); 37 | 38 | return { 39 | ...httpConfig.DELETE_SUCCESS, 40 | data: result 41 | }; 42 | } catch (err) { 43 | fastify.log.error(err); 44 | return httpConfig.DELETE_FAIL; 45 | } 46 | } 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/roleDetail.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | code: fnSchema('code') 11 | }, 12 | required: ['code'] 13 | }, 14 | // 执行函数 15 | apiHandler: async (req) => { 16 | try { 17 | const roleModel = fastify.mysql.table('sys_role').where('code', req.body.code); 18 | 19 | const result = await roleModel.clone().selectOne(); 20 | 21 | if (!result?.id) { 22 | return { 23 | ...httpConfig.SELECT_FAIL, 24 | msg: '没有查到角色信息' 25 | }; 26 | } 27 | 28 | return { 29 | ...httpConfig.SELECT_SUCCESS, 30 | data: result 31 | }; 32 | } catch (err) { 33 | fastify.log.error(err); 34 | return httpConfig.SELECT_FAIL; 35 | } 36 | } 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/roleInsert.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | import { tableData } from '../../tables/role.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | code: fnSchema(tableData.code), 12 | name: fnSchema(tableData.name), 13 | sort: fnSchema(tableData.sort), 14 | describe: fnSchema(tableData.describe), 15 | menu_ids: fnSchema(tableData.menu_ids), 16 | api_ids: fnSchema(tableData.api_ids) 17 | }, 18 | required: ['name', 'code'] 19 | }, 20 | // 执行函数 21 | apiHandler: async (req) => { 22 | try { 23 | const roleModel = fastify.mysql.table('sys_role'); 24 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 25 | 26 | const roleData = await roleModel // 27 | .clone() 28 | .where('name', req.body.name) 29 | .orWhere('code', req.body.code) 30 | .selectOne(['id']); 31 | 32 | if (roleData?.id) { 33 | return { 34 | ...httpConfig.INSERT_FAIL, 35 | msg: '角色名称或编码已存在' 36 | }; 37 | } 38 | 39 | const result = await roleModel.clone().insertData({ 40 | code: req.body.code, 41 | name: req.body.name, 42 | sort: req.body.sort, 43 | describe: req.body.describe, 44 | menu_ids: req.body.menu_ids, 45 | api_ids: req.body.api_ids 46 | }); 47 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req))); 48 | 49 | await fastify.cacheRoleData(); 50 | 51 | return { 52 | ...httpConfig.INSERT_SUCCESS, 53 | data: result 54 | }; 55 | } catch (err) { 56 | fastify.log.error(err); 57 | return httpConfig.INSERT_FAIL; 58 | } 59 | } 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/roleSelectAll.js: -------------------------------------------------------------------------------- 1 | import { fnRoute } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: {} 10 | }, 11 | // 执行函数 12 | apiHandler: async (req) => { 13 | try { 14 | const roleModel = fastify.mysql // 15 | .table('sys_role') 16 | .modify(function (db) { 17 | // 如果不是开发管理员查询,则排除掉开发角色 18 | if (req.session.role !== 'dev') { 19 | db.where('code', '<>', 'dev'); 20 | } 21 | }); 22 | 23 | const rows = await roleModel.clone().selectAll(); 24 | 25 | return { 26 | ...httpConfig.SELECT_SUCCESS, 27 | data: { 28 | rows: rows 29 | } 30 | }; 31 | } catch (err) { 32 | fastify.log.error(err); 33 | return httpConfig.SELECT_FAIL; 34 | } 35 | } 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/roleSelectPage.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | page: fnSchema('page'), 11 | limit: fnSchema('limit'), 12 | keyword: fnSchema('keyword') 13 | }, 14 | required: [] 15 | }, 16 | // 执行函数 17 | apiHandler: async (req) => { 18 | try { 19 | const roleModel = fastify.mysql // 20 | .table('sys_role') 21 | .where('code', '<>', 'dev') 22 | .modify((db) => { 23 | if (req.body.keyword) { 24 | db.whereLike('name', `${req.body.keyword}`); 25 | } 26 | }); 27 | 28 | const { totalCount } = await roleModel.clone().selectCount(); 29 | const rows = await roleModel 30 | // 31 | .clone() 32 | .orderBy([ 33 | { column: 'sort', order: 'asc' }, 34 | { column: 'created_at', order: 'desc' } 35 | ]) 36 | .selectData(req.body.page, req.body.limit); 37 | 38 | return { 39 | ...httpConfig.SELECT_SUCCESS, 40 | data: { 41 | total: totalCount, 42 | rows: rows, 43 | page: req.body.page, 44 | limit: req.body.limit 45 | } 46 | }; 47 | } catch (err) { 48 | fastify.log.error(err); 49 | return httpConfig.SELECT_FAIL; 50 | } 51 | } 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/funpi/apis/admin/roleUpdate.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | import { tableData } from '../../tables/role.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | id: fnSchema('id'), 12 | code: fnSchema(tableData.code), 13 | name: fnSchema(tableData.name), 14 | sort: fnSchema(tableData.sort), 15 | describe: fnSchema(tableData.describe), 16 | menu_ids: fnSchema(tableData.menu_ids), 17 | api_ids: fnSchema(tableData.api_ids) 18 | }, 19 | required: ['id'] 20 | }, 21 | // 执行函数 22 | apiHandler: async (req) => { 23 | try { 24 | const roleModel = fastify.mysql.table('sys_role'); 25 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 26 | 27 | const roleData = await roleModel // 28 | .clone() 29 | .where('name', req.body.name) 30 | .orWhere('code', req.body.code) 31 | .selectOne(['id']); 32 | 33 | // 编码存在且 id 不等于当前角色 34 | if (roleData?.id && roleData?.id !== req.body.id) { 35 | return { 36 | ...httpConfig.INSERT_FAIL, 37 | msg: '角色名称或编码已存在' 38 | }; 39 | } 40 | 41 | const result = await roleModel.clone().where({ id: req.body.id }).updateData({ 42 | code: req.body.code, 43 | name: req.body.name, 44 | sort: req.body.sort, 45 | describe: req.body.describe, 46 | menu_ids: req.body.menu_ids, 47 | api_ids: req.body.api_ids 48 | }); 49 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req))); 50 | 51 | await fastify.cacheRoleData(); 52 | 53 | return { 54 | ...httpConfig.UPDATE_SUCCESS, 55 | data: result 56 | }; 57 | } catch (err) { 58 | fastify.log.error(err); 59 | return httpConfig.UPDATE_FAIL; 60 | } 61 | } 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/_meta.js: -------------------------------------------------------------------------------- 1 | export const metaConfig = { 2 | dirName: '字典管理', 3 | apiNames: { 4 | categoryDelete: '删除字典分类', 5 | categoryInsert: '添加字典分类', 6 | categorySelectAll: '查询字典分类-所有', 7 | categorySelectPage: '查询字典分类-分页', 8 | categoryUpdate: '更新字典分类', 9 | dictDelete: '删除字典', 10 | dictInsert: '添加字典', 11 | dictSelectAll: '查询字典-所有', 12 | dictSelectPage: '查询字典-分页', 13 | dictUpdate: '更新字典' 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/categoryDelete.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | id: fnSchema('id') 11 | }, 12 | required: ['id'] 13 | }, 14 | // 执行函数 15 | apiHandler: async (req) => { 16 | try { 17 | const dictCategoryModel = fastify.mysql.table('sys_dict_category').where({ id: req.body.id }); 18 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 19 | 20 | const dictModel = fastify.mysql.table('sys_dict'); 21 | 22 | const dictCategoryData = await dictCategoryModel.clone().selectOne(['id']); 23 | 24 | if (!dictCategoryData?.id) { 25 | return httpConfig.NO_DATA; 26 | } 27 | 28 | const childrenDict = await dictModel.clone().where({ category_id: req.body.id }).selectOne(['id']); 29 | if (childrenDict?.id) { 30 | return { 31 | ...httpConfig.DELETE_FAIL, 32 | msg: '此分类下有字典数据,无法删除' 33 | }; 34 | } 35 | 36 | const result = await dictCategoryModel.clone().deleteData(); 37 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req))); 38 | 39 | return { 40 | ...httpConfig.DELETE_SUCCESS, 41 | data: result 42 | }; 43 | } catch (err) { 44 | fastify.log.error(err); 45 | return httpConfig.DELETE_FAIL; 46 | } 47 | } 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/categoryInsert.js: -------------------------------------------------------------------------------- 1 | import { camelCase } from 'es-toolkit'; 2 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog } from '../../utils/index.js'; 3 | import { httpConfig } from '../../config/http.js'; 4 | import { tableData } from '../../tables/dictCategory.js'; 5 | 6 | export default async (fastify) => { 7 | fnRoute(import.meta.url, fastify, { 8 | // 请求参数约束 9 | schemaRequest: { 10 | type: 'object', 11 | properties: { 12 | code: fnSchema(tableData.code), 13 | name: fnSchema(tableData.name), 14 | describe: fnSchema(tableData.describe) 15 | }, 16 | required: ['code', 'name'] 17 | }, 18 | // 执行函数 19 | apiHandler: async (req) => { 20 | try { 21 | const dictCategoryModel = fastify.mysql.table('sys_dict_category'); 22 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 23 | 24 | const dictCategoryData = await dictCategoryModel 25 | .clone() 26 | .where({ code: camelCase(req.body.code) }) 27 | .selectOne(['id']); 28 | 29 | if (dictCategoryData?.id) { 30 | return { 31 | ...httpConfig.INSERT_FAIL, 32 | msg: '当前编号已存在' 33 | }; 34 | } 35 | 36 | const result = await dictCategoryModel.insertData({ 37 | code: camelCase(req.body.code), 38 | name: req.body.name, 39 | describe: req.body.describe 40 | }); 41 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req))); 42 | 43 | return { 44 | ...httpConfig.INSERT_SUCCESS, 45 | data: result 46 | }; 47 | } catch (err) { 48 | fastify.log.error(err); 49 | return httpConfig.INSERT_FAIL; 50 | } 51 | } 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/categorySelectAll.js: -------------------------------------------------------------------------------- 1 | import { fnRoute } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: {} 10 | }, 11 | // 执行函数 12 | apiHandler: async (req) => { 13 | try { 14 | const dictCategoryModel = fastify.mysql // 15 | .table('sys_dict_category') 16 | .modify(function (db) {}); 17 | 18 | const rows = await dictCategoryModel.clone().selectAll(); 19 | 20 | return { 21 | ...httpConfig.SELECT_SUCCESS, 22 | data: { 23 | rows: rows 24 | } 25 | }; 26 | } catch (err) { 27 | fastify.log.error(err); 28 | return httpConfig.SELECT_FAIL; 29 | } 30 | } 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/categorySelectPage.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | page: fnSchema('page'), 11 | limit: fnSchema('limit'), 12 | keyword: fnSchema('keyword') 13 | }, 14 | required: [] 15 | }, 16 | // 执行函数 17 | apiHandler: async (req) => { 18 | try { 19 | const dictCategoryModel = fastify.mysql // 20 | .table('sys_dict_category') 21 | .modify(function (db) { 22 | if (req.body.keyword) { 23 | db.where('name', 'like', `%${req.body.keyword}%`); 24 | } 25 | }); 26 | 27 | // 记录总数 28 | const { totalCount } = await dictCategoryModel.clone().selectCount(); 29 | 30 | // 记录列表 31 | const rows = await dictCategoryModel 32 | // 33 | .clone() 34 | .orderBy('created_at', 'desc') 35 | .selectData(req.body.page, req.body.limit); 36 | 37 | return { 38 | ...httpConfig.SELECT_SUCCESS, 39 | data: { 40 | total: totalCount, 41 | rows: rows, 42 | page: req.body.page, 43 | limit: req.body.limit 44 | } 45 | }; 46 | } catch (err) { 47 | fastify.log.error(err); 48 | return httpConfig.SELECT_FAIL; 49 | } 50 | } 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/categoryUpdate.js: -------------------------------------------------------------------------------- 1 | import { camelCase } from 'es-toolkit'; 2 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog } from '../../utils/index.js'; 3 | import { httpConfig } from '../../config/http.js'; 4 | import { tableData } from '../../tables/dictCategory.js'; 5 | 6 | export default async (fastify) => { 7 | fnRoute(import.meta.url, fastify, { 8 | // 请求参数约束 9 | schemaRequest: { 10 | type: 'object', 11 | properties: { 12 | id: fnSchema('page'), 13 | code: fnSchema(tableData.code), 14 | name: fnSchema(tableData.name), 15 | describe: fnSchema(tableData.describe) 16 | }, 17 | required: ['id', 'code'] 18 | }, 19 | // 执行函数 20 | apiHandler: async (req) => { 21 | try { 22 | const dictCategoryModel = fastify.mysql.table('sys_dict_category'); 23 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 24 | 25 | const dictCategoryData = await dictCategoryModel 26 | .clone() 27 | .where({ code: camelCase(req.body.code) }) 28 | .selectOne(['id']); 29 | 30 | if (dictCategoryData?.id && dictCategoryData?.id !== req.body.id) { 31 | return { 32 | ...httpConfig.FAIL, 33 | msg: '当前编号已存在' 34 | }; 35 | } 36 | 37 | const result = await dictCategoryModel 38 | .clone() 39 | .where({ id: req.body.id }) 40 | .updateData({ 41 | code: camelCase(req.body.code), 42 | name: req.body.name, 43 | describe: req.body.describe 44 | }); 45 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req))); 46 | 47 | return httpConfig.UPDATE_SUCCESS; 48 | } catch (err) { 49 | fastify.log.error(err); 50 | return httpConfig.UPDATE_FAIL; 51 | } 52 | } 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/dictDelete.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: { 10 | id: fnSchema('id') 11 | }, 12 | required: ['id'] 13 | }, 14 | // 执行函数 15 | apiHandler: async (req) => { 16 | try { 17 | const dictModel = fastify.mysql.table('sys_dict').where({ id: req.body.id }); 18 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 19 | 20 | const dictData = await dictModel.clone().selectOne(['id']); 21 | if (!dictData?.id) { 22 | return httpConfig.NO_DATA; 23 | } 24 | 25 | const result = await dictModel.clone().deleteData(); 26 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req))); 27 | 28 | return { 29 | ...httpConfig.DELETE_SUCCESS, 30 | data: result 31 | }; 32 | } catch (err) { 33 | fastify.log.error(err); 34 | return httpConfig.DELETE_FAIL; 35 | } 36 | } 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/dictInsert.js: -------------------------------------------------------------------------------- 1 | import { camelCase } from 'es-toolkit'; 2 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog } from '../../utils/index.js'; 3 | import { httpConfig } from '../../config/http.js'; 4 | import { tableData } from '../../tables/dict.js'; 5 | 6 | export default async (fastify) => { 7 | fnRoute(import.meta.url, fastify, { 8 | // 请求参数约束 9 | schemaRequest: { 10 | type: 'object', 11 | properties: { 12 | category_id: fnSchema(tableData.category_id), 13 | category_code: fnSchema(tableData.category_code), 14 | code: fnSchema(tableData.code), 15 | name: fnSchema(tableData.name), 16 | value: fnSchema(tableData.value), 17 | symbol: fnSchema(tableData.symbol), 18 | thumbnail: fnSchema(tableData.thumbnail), 19 | describe: fnSchema(tableData.describe) 20 | }, 21 | required: ['category_id', 'category_code', 'code', 'name', 'value', 'symbol'] 22 | }, 23 | // 执行函数 24 | apiHandler: async (req) => { 25 | try { 26 | // 如果传的值是数值类型,则判断是否为有效数值 27 | if (req.body.symbol === 'number') { 28 | if (Number.isNaN(Number(req.body.value)) === true) { 29 | return { 30 | ...httpConfig.INSERT_FAIL, 31 | msg: '字典值不是一个数字类型' 32 | }; 33 | } 34 | } 35 | 36 | const dictModel = fastify.mysql.table('sys_dict'); 37 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 38 | 39 | const result = await dictModel.insertData({ 40 | category_id: req.body.category_id, 41 | category_code: camelCase(req.body.category_code), 42 | code: camelCase(req.body.code), 43 | name: req.body.name, 44 | value: req.body.value, 45 | symbol: req.body.symbol, 46 | thumbnail: req.body.thumbnail, 47 | describe: req.body.describe 48 | }); 49 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req))); 50 | 51 | return { 52 | ...httpConfig.INSERT_SUCCESS, 53 | data: result 54 | }; 55 | } catch (err) { 56 | fastify.log.error(err); 57 | return httpConfig.INSERT_FAIL; 58 | } 59 | } 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/dictSelectAll.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | import { tableData } from '../../tables/dict.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | category_code: fnSchema(tableData.category_code) 12 | } 13 | }, 14 | // 执行函数 15 | apiHandler: async (req) => { 16 | try { 17 | const dictModel = fastify.mysql 18 | .table('sys_dict') 19 | .where('category_code', req.body.category_code) 20 | .modify(function (db) {}); 21 | 22 | const rowsTemp = await dictModel.clone().selectAll(); 23 | 24 | const rows = rowsTemp?.map((item) => { 25 | if (item.symbol === 'number') { 26 | item.value = Number(item.value); 27 | } 28 | return item; 29 | }); 30 | return { 31 | ...httpConfig.SELECT_SUCCESS, 32 | data: { 33 | rows: rows 34 | } 35 | }; 36 | } catch (err) { 37 | fastify.log.error(err); 38 | return httpConfig.SELECT_FAIL; 39 | } 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/dictSelectPage.js: -------------------------------------------------------------------------------- 1 | import { fnRoute, fnSchema } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | import { tableData } from '../../tables/dict.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | type: 'object', 10 | properties: { 11 | page: fnSchema('page'), 12 | limit: fnSchema('limit'), 13 | keyword: fnSchema('keyword'), 14 | category_code: fnSchema(tableData.category_code) 15 | }, 16 | required: ['category_code'] 17 | }, 18 | // 执行函数 19 | apiHandler: async (req) => { 20 | try { 21 | const dictModel = fastify.mysql // 22 | .table('sys_dict') 23 | .where('category_code', req.body.category_code) 24 | .modify(function (db) { 25 | if (req.body.keyword) { 26 | db.where('name', 'like', `%${req.body.keyword}%`); 27 | } 28 | }); 29 | 30 | // 记录总数 31 | const { totalCount } = await dictModel.clone().selectCount(); 32 | 33 | // 记录列表 34 | const rowsTemp = await dictModel 35 | // 36 | .clone() 37 | .orderBy('created_at', 'desc') 38 | .selectData(req.body.page, req.body.limit); 39 | 40 | // 处理数字符号强制转换为数字值 41 | const rows = rowsTemp?.map((item) => { 42 | if (item.symbol === 'number') { 43 | item.value = Number(item.value); 44 | } 45 | return item; 46 | }); 47 | 48 | return { 49 | ...httpConfig.SELECT_SUCCESS, 50 | data: { 51 | total: totalCount, 52 | rows: rows, 53 | page: req.body.page, 54 | limit: req.body.limit 55 | } 56 | }; 57 | } catch (err) { 58 | fastify.log.error(err); 59 | return httpConfig.SELECT_FAIL; 60 | } 61 | } 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /packages/funpi/apis/dict/dictUpdate.js: -------------------------------------------------------------------------------- 1 | import { camelCase } from 'es-toolkit'; 2 | import { fnRoute, fnSchema, fnDataClear, fnRequestLog } from '../../utils/index.js'; 3 | import { httpConfig } from '../../config/http.js'; 4 | import { tableData } from '../../tables/dict.js'; 5 | 6 | export default async (fastify) => { 7 | fnRoute(import.meta.url, fastify, { 8 | // 请求参数约束 9 | schemaRequest: { 10 | type: 'object', 11 | properties: { 12 | id: fnSchema('id'), 13 | category_id: fnSchema(tableData.category_id), 14 | category_code: fnSchema(tableData.category_code), 15 | code: fnSchema(tableData.code), 16 | name: fnSchema(tableData.name), 17 | value: fnSchema(tableData.value), 18 | symbol: fnSchema(tableData.symbol), 19 | thumbnail: fnSchema(tableData.thumbnail), 20 | describe: fnSchema(tableData.describe) 21 | }, 22 | required: ['id'] 23 | }, 24 | // 执行函数 25 | apiHandler: async (req) => { 26 | try { 27 | if (req.body.type === 'number') { 28 | if (Number.isNaN(Number(req.body.value)) === true) { 29 | return { 30 | ...httpConfig.UPDATE_FAIL, 31 | msg: '字典值不是一个数字类型' 32 | }; 33 | } 34 | } 35 | const dictModel = fastify.mysql.table('sys_dict').modify(function (db) {}); 36 | const adminActionLogModel = fastify.mysql.table('sys_admin_action_log'); 37 | 38 | const result = await dictModel 39 | .clone() 40 | .where({ id: req.body.id }) 41 | .updateData({ 42 | category_id: req.body.category_id, 43 | category_code: camelCase(req.body.category_code), 44 | code: camelCase(req.body.code), 45 | name: req.body.name, 46 | value: req.body.value, 47 | symbol: req.body.symbol, 48 | thumbnail: req.body.thumbnail, 49 | describe: req.body.describe 50 | }); 51 | await adminActionLogModel.clone().insertData(fnDataClear(fnRequestLog(req))); 52 | 53 | return httpConfig.UPDATE_SUCCESS; 54 | } catch (err) { 55 | fastify.log.error(err); 56 | return httpConfig.UPDATE_FAIL; 57 | } 58 | } 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/funpi/apis/tool/_meta.js: -------------------------------------------------------------------------------- 1 | export const metaConfig = { 2 | dirName: '工具', 3 | apiNames: { 4 | sendMail: '发送邮件', 5 | tokenCheck: '令牌检测', 6 | getOnline: '获取在线统计', 7 | setOnline: '设置在线统计' 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /packages/funpi/apis/tool/sendMail.js: -------------------------------------------------------------------------------- 1 | import { randomInt } from 'es-toolkit'; 2 | import { fnRoute, fnSchema } from '../../utils/index.js'; 3 | import { httpConfig } from '../../config/http.js'; 4 | 5 | export default async (fastify) => { 6 | fnRoute(import.meta.url, fastify, { 7 | // 请求参数约束 8 | schemaRequest: { 9 | oneOf: [ 10 | { 11 | title: '发送验证码邮件', 12 | type: 'object', 13 | properties: { 14 | to_email: fnSchema({ name: '发送给谁', type: 'string', min: 5, max: 100 }), 15 | subject: fnSchema({ name: '邮件主题', type: 'string', min: 1, max: 300 }), 16 | verify_name: fnSchema({ name: '验证码名称', type: 'string', min: 2, max: 30, pattern: '^[a-z][a-zA-Z0-9]*' }) 17 | }, 18 | required: ['to_email', 'subject', 'verify_name'] 19 | }, 20 | { 21 | title: '发送普通邮件', 22 | type: 'object', 23 | properties: { 24 | to_email: fnSchema({ name: '发送给谁', type: 'string', min: 5, max: 100 }), 25 | subject: fnSchema({ name: '邮件主题', type: 'string', min: 1, max: 300 }), 26 | content: fnSchema({ name: '邮件内容', type: 'string', min: 1, max: 10000 }) 27 | }, 28 | required: ['to_email', 'subject', 'content'] 29 | } 30 | ] 31 | }, 32 | // 执行函数 33 | apiHandler: async (req) => { 34 | try { 35 | const mailLogModel = fastify.mysql.table('sys_mail_log'); 36 | // 普通发送 37 | if (req.body.content) { 38 | await fastify.sendMail({ 39 | to: req.body.to_email, 40 | subject: req.body.subject, 41 | text: req.body.content 42 | }); 43 | await mailLogModel.clone().insertData({ 44 | login_email: process.env.MAIL_USER, 45 | from_name: process.env.MAIL_FROM_NAME, 46 | from_email: process.env.MAIL_FROM_EMAIL, 47 | to_email: req.body.to_email, 48 | email_type: 'common', 49 | text_content: req.body.content 50 | }); 51 | return { 52 | ...httpConfig.SUCCESS, 53 | msg: '邮件已发送', 54 | from: 'new' 55 | }; 56 | } 57 | 58 | // 发送验证码 59 | if (req.body.verify_name) { 60 | // 如果已经发送过 61 | const existsVerifyCode = await fastify.redisGet(`${req.body.verify_name}:${req.body.to_email}`); 62 | if (existsVerifyCode) { 63 | return { 64 | ...httpConfig.SUCCESS, 65 | msg: '邮箱验证码已发送(5 分钟有效)', 66 | from: 'cache' 67 | }; 68 | } 69 | 70 | // 如果没有发送过 71 | const cacheVerifyCode = randomInt(100000, 999999); 72 | await fastify.redisSet(`${req.body.verify_name}:${req.body.to_email}`, cacheVerifyCode, 60 * 5); 73 | await fastify.sendMail({ 74 | to: req.body.to_email, 75 | subject: req.body.subject, 76 | text: req.body.subject + ':' + cacheVerifyCode 77 | }); 78 | await mailLogModel.clone().insertData({ 79 | login_email: process.env.MAIL_USER, 80 | from_name: process.env.MAIL_FROM_NAME, 81 | from_email: process.env.MAIL_FROM_EMAIL, 82 | to_email: req.body.to_email, 83 | email_type: 'verify', 84 | text_content: '******' 85 | }); 86 | return { 87 | ...httpConfig.SUCCESS, 88 | msg: '邮箱验证码已发送(5 分钟有效)', 89 | from: 'new' 90 | }; 91 | } 92 | } catch (err) { 93 | fastify.log.error(err); 94 | return httpConfig.FAIL; 95 | } 96 | } 97 | }); 98 | }; 99 | -------------------------------------------------------------------------------- /packages/funpi/apis/tool/tokenCheck.js: -------------------------------------------------------------------------------- 1 | import { fnRoute } from '../../utils/index.js'; 2 | import { httpConfig } from '../../config/http.js'; 3 | 4 | export default async (fastify) => { 5 | fnRoute(import.meta.url, fastify, { 6 | // 请求参数约束 7 | schemaRequest: { 8 | type: 'object', 9 | properties: {}, 10 | required: [] 11 | }, 12 | // 执行函数 13 | apiHandler: async (req) => { 14 | try { 15 | try { 16 | const jwtData = await req.jwtVerify(); 17 | return { 18 | ...httpConfig.SUCCESS, 19 | data: { 20 | state: 'yes' 21 | }, 22 | detail: jwtData 23 | }; 24 | } catch (err) { 25 | fastify.log.error(err); 26 | return { 27 | ...httpConfig.SUCCESS, 28 | data: { 29 | state: 'no' 30 | } 31 | }; 32 | } 33 | } catch (err) { 34 | fastify.log.error(err); 35 | return httpConfig.FAIL; 36 | } 37 | } 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/funpi/bootstrap/auth.js: -------------------------------------------------------------------------------- 1 | // 内部模块 2 | import { existsSync } from 'node:fs'; 3 | import { join } from 'pathe'; 4 | // 外部模块 5 | import fp from 'fastify-plugin'; 6 | import picomatch from 'picomatch'; 7 | import { find as es_find } from 'es-toolkit/compat'; 8 | // 配置文件 9 | import { appDir } from '../config/path.js'; 10 | import { httpConfig } from '../config/http.js'; 11 | // 工具函数 12 | import { fnApiCheck, fnDataClear } from '../utils/index.js'; 13 | 14 | async function plugin(fastify) { 15 | fastify.addHook('onRequest', async (req) => { 16 | if (!req.headers['Authorization'] && !req.headers['authorization']) { 17 | req.headers['Authorization'] = 'Bearer funpi'; 18 | } 19 | }); 20 | fastify.addHook('preHandler', async (req, res) => { 21 | try { 22 | // 如果是收藏图标,则直接通过 23 | if (req.url === 'favicon.ico') return; 24 | if (req.url === '/') { 25 | res.send({ 26 | code: 0, 27 | msg: `${process.env.APP_NAME} 接口程序已启动` 28 | }); 29 | return; 30 | } 31 | if (req.url.startsWith('/swagger/')) return; 32 | 33 | /* --------------------------------- 请求资源判断 --------------------------------- */ 34 | if (req.url.startsWith('/public')) { 35 | const filePath = join(appDir, req.url); 36 | if (existsSync(filePath) === true) { 37 | return; 38 | } else { 39 | // 文件不存在 40 | res.send(httpConfig.NO_FILE); 41 | return; 42 | } 43 | } 44 | 45 | if (!req.routeOptions?.url || !req.routeOptions?.url?.startsWith('/api/')) { 46 | res.send(httpConfig.NO_API); 47 | return; 48 | } 49 | 50 | if (req.routeOptions.url === '/api/funpi/admin/adminLogin') return; 51 | if (req.routeOptions.url === '/api/funpi/tool/tokenCheck') return; 52 | 53 | /* --------------------------------- 解析用户登录参数 --------------------------------- */ 54 | let isAuthFail = false; 55 | try { 56 | await req.jwtVerify(); 57 | } catch (err) { 58 | req.session = { 59 | id: 0, 60 | username: 'visitor', 61 | nickname: '游客', 62 | role_type: 'user', 63 | role: 'visitor' 64 | }; 65 | isAuthFail = true; 66 | } 67 | 68 | /* ---------------------------------- 日志记录 ---------------------------------- */ 69 | 70 | fastify.log.warn({ 71 | apiPath: req?.url, 72 | body: fnDataClear(req.body), 73 | session: req?.session, 74 | reqId: req?.id 75 | }); 76 | 77 | /* --------------------------------- 接口存在性判断 -------------------------------- */ 78 | const allApiNames = await fastify.redisGet('cacheData:apiNames'); 79 | 80 | if (allApiNames.includes(req.routeOptions.url) === false) { 81 | res.send(httpConfig.NO_API); 82 | return; 83 | } 84 | 85 | /* --------------------------------- 上传参数检测 --------------------------------- */ 86 | if (process.env.PARAMS_CHECK === '1') { 87 | const result = await fnApiCheck(req); 88 | if (result.code !== 0) { 89 | res.send({ 90 | ...httpConfig.PARAMS_SIGN_FAIL, 91 | detail: result?.msg || '' 92 | }); 93 | return; 94 | } 95 | } 96 | 97 | /* ---------------------------------- 角色接口权限判断 --------------------------------- */ 98 | // 如果接口不在白名单中,则判断用户是否有接口访问权限 99 | const userApis = await fastify.getUserApis(req.session); 100 | 101 | const hasApi = es_find(userApis, ['value', req.routeOptions.url]); 102 | /* --------------------------------- 接口登录检测 --------------------------------- */ 103 | 104 | if (!hasApi) { 105 | res.send({ 106 | ...httpConfig.FAIL, 107 | msg: `您没有 [ ${req?.routeOptions?.schema?.summary || req.routeOptions.url} ] 接口的操作权限`, 108 | login: isAuthFail === false ? 'yes' : 'no' 109 | }); 110 | return; 111 | } 112 | } catch (err) { 113 | fastify.log.error(err); 114 | res.send({ 115 | ...httpConfig.FAIL, 116 | msg: err.msg || '认证异常', 117 | other: err.other || '' 118 | }); 119 | return res; 120 | } 121 | }); 122 | } 123 | export default fp(plugin, { name: 'funpiAuth', dependencies: ['funpiCors', 'funpiMysql', 'funpiTool'] }); 124 | -------------------------------------------------------------------------------- /packages/funpi/bootstrap/cors.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import fastifyCors from '@fastify/cors'; 3 | 4 | async function plugin(fastify) { 5 | await fastify.register(fastifyCors, function () { 6 | return (req, callback) => { 7 | // 默认跨域,如果需要指定请求前缀,可以被传入的参数覆盖 8 | const newCorsConfig = { 9 | origin: req.headers.origin || req.headers.host || '*', 10 | methods: ['GET', 'OPTIONS', 'POST'], 11 | allowedHeaders: ['Content-Type', 'Authorization', 'authorization', 'token'], 12 | exposedHeaders: ['Content-Range', 'X-Content-Range', 'Authorization', 'authorization', 'token'], 13 | preflightContinue: false, 14 | strictPreflight: false, 15 | preflight: true, 16 | optionsSuccessStatus: 204, 17 | credentials: false 18 | }; 19 | 20 | callback(null, newCorsConfig); 21 | }; 22 | }); 23 | } 24 | export default fp(plugin, { name: 'funpiCors' }); 25 | -------------------------------------------------------------------------------- /packages/funpi/bootstrap/mail.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import nodemailer from 'nodemailer'; 3 | 4 | async function plugin(fastify) { 5 | const mailTransport = await nodemailer.createTransport({ 6 | host: process.env.MAIL_HOST, 7 | port: process.env.MAIL_PORT, 8 | pool: process.env.MAIL_POOL === '1' ? true : false, 9 | secure: process.env.MAIL_SECURE === '1' ? true : false, 10 | auth: { 11 | user: process.env.MAIL_USER, 12 | pass: process.env.MAIL_PASS 13 | } 14 | }); 15 | 16 | // 发送邮件 17 | function sendMail(params) { 18 | return new Promise((resolve, reject) => { 19 | try { 20 | const result = mailTransport.sendMail({ 21 | from: { 22 | name: process.env.MAIL_FROM_NAME, 23 | address: process.env.MAIL_FROM_EMAIL 24 | }, 25 | ...params 26 | }); 27 | resolve(result); 28 | } catch (err) { 29 | reject(err); 30 | } 31 | }); 32 | } 33 | 34 | fastify.decorate('sendMail', sendMail); 35 | } 36 | export default fp(plugin, { name: 'funpiMail' }); 37 | -------------------------------------------------------------------------------- /packages/funpi/bootstrap/redis.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import { createClient } from 'redis'; 3 | 4 | async function plugin(fastify, opts) { 5 | if (fastify.redis) { 6 | return; 7 | } 8 | const client = await createClient({ 9 | username: process.env.REDIS_USERNAME, 10 | password: process.env.REDIS_PASSWORD, 11 | database: Number(process.env.REDIS_DB), 12 | socket: { 13 | reconnectStrategy: (retries) => { 14 | return false; 15 | } 16 | } 17 | }) 18 | .on('error', (err) => { 19 | fastify.log.error({ msg: 'Redis连接失败', detail: err }); 20 | process.exit(1); 21 | }) 22 | .connect(); 23 | 24 | fastify.decorate('redis', client); 25 | } 26 | export default fp(plugin, { name: 'funpiRedis' }); 27 | -------------------------------------------------------------------------------- /packages/funpi/bootstrap/tool.js: -------------------------------------------------------------------------------- 1 | // 外部模块 2 | import fp from 'fastify-plugin'; 3 | // 配置文件 4 | 5 | async function plugin(fastify) { 6 | // 设置 redis 7 | const redisSet = async (key, value, second = 0) => { 8 | try { 9 | if (second > 0) { 10 | await fastify.redis.set(`${process.env.REDIS_KEY_PREFIX}:${key}`, JSON.stringify(value), 'EX', second); 11 | } else { 12 | await fastify.redis.set(`${process.env.REDIS_KEY_PREFIX}:${key}`, JSON.stringify(value)); 13 | } 14 | } catch (err) { 15 | fastify.log.warn(err); 16 | } 17 | }; 18 | 19 | // 获取 redis 20 | const redisGet = async (key) => { 21 | try { 22 | const result = await fastify.redis.get(`${process.env.REDIS_KEY_PREFIX}:${key}`); 23 | return JSON.parse(result); 24 | } catch (err) { 25 | fastify.log.warn(err); 26 | } 27 | }; 28 | // 删除 redis 29 | const redisDel = async (key) => { 30 | try { 31 | await fastify.redis.del(`${process.env.REDIS_KEY_PREFIX}:${key}`); 32 | } catch (err) { 33 | fastify.log.warn(err); 34 | } 35 | }; 36 | 37 | const getUserApis = async (session) => { 38 | if (!session) return []; 39 | // 提取当前用户的角色码组 40 | 41 | // 提取所有角色拥有的接口 42 | let apiIds = []; 43 | const dataRoleCodes = await redisGet('cacheData:role'); 44 | dataRoleCodes.forEach((item) => { 45 | if (session.role === item.code) { 46 | apiIds = item.api_ids 47 | .split(',') 48 | .filter((id) => id !== '') 49 | .map((id) => Number(id)); 50 | } 51 | }); 52 | 53 | // 将接口进行唯一性处理 54 | const userApiIds = [...new Set(apiIds)]; 55 | const dataApi = await redisGet('cacheData:api'); 56 | // 最终的用户接口列表 57 | const result = dataApi.filter((item) => { 58 | return userApiIds.includes(item.id); 59 | }); 60 | return result; 61 | }; 62 | 63 | const getUserMenus = async (session) => { 64 | try { 65 | if (!session) return []; 66 | 67 | // 所有菜单 ID 68 | let menuIds = []; 69 | 70 | const dataRoleCodes = await redisGet('cacheData:role'); 71 | dataRoleCodes.forEach((item) => { 72 | if (session.role === item.code) { 73 | menuIds = item.menu_ids 74 | .split(',') 75 | .filter((id) => id !== '') 76 | .map((id) => Number(id)); 77 | } 78 | }); 79 | 80 | const userMenuIds = [...new Set(menuIds)]; 81 | const dataMenu = await redisGet('cacheData:menu'); 82 | 83 | const result = dataMenu.filter((item) => { 84 | return userMenuIds.includes(item.id); 85 | }); 86 | return result; 87 | } catch (err) { 88 | fastify.log.error(err); 89 | } 90 | }; 91 | 92 | const cacheMenuData = async () => { 93 | // 菜单列表 94 | const dataMenu = await fastify.mysql.table('sys_menu').selectAll(); 95 | 96 | // 菜单树数据 97 | await redisSet('cacheData:menu', []); 98 | await redisSet('cacheData:menu', dataMenu); 99 | }; 100 | 101 | const cacheApiData = async () => { 102 | // 菜单列表 103 | const dataApi = await fastify.mysql.table('sys_api').selectAll(); 104 | 105 | // 白名单接口 106 | // const apiWhiteLists = dataApi.filter((item) => item.state === 1).map((item) => `${item.value}`); 107 | // 黑名单接口 108 | // const apiBlackLists = dataApi.filter((item) => item.state === 2).map((item) => `${item.value}`); 109 | 110 | // 接口树数据 111 | await redisSet('cacheData:api', []); 112 | await redisSet('cacheData:api', dataApi); 113 | 114 | // 接口名称缓存 115 | await redisSet('cacheData:apiNames', []); 116 | await redisSet( 117 | 'cacheData:apiNames', 118 | dataApi.filter((item) => item.pid !== 0).map((item) => `${item.value}`) 119 | ); 120 | }; 121 | 122 | const cacheRoleData = async () => { 123 | // 角色类别 124 | const dataRole = await fastify.mysql.table('sys_role').selectAll(); 125 | 126 | await redisSet('cacheData:role', []); 127 | await redisSet('cacheData:role', dataRole); 128 | }; 129 | 130 | // 设置和获取缓存数据 131 | fastify.decorate('redisSet', redisSet); 132 | fastify.decorate('redisGet', redisGet); 133 | fastify.decorate('redisDel', redisDel); 134 | // 获取当前登录用户可操作的接口列表 135 | fastify.decorate('getUserApis', getUserApis); 136 | // 获取用户的菜单 137 | fastify.decorate('getUserMenus', getUserMenus); 138 | // 设置权限数据 139 | fastify.decorate('cacheMenuData', cacheMenuData); 140 | // 设置权限数据 141 | fastify.decorate('cacheApiData', cacheApiData); 142 | // 设置角色数据 143 | fastify.decorate('cacheRoleData', cacheRoleData); 144 | } 145 | export default fp(plugin, { name: 'funpiTool', dependencies: ['funpiRedis', 'funpiMysql'] }); 146 | -------------------------------------------------------------------------------- /packages/funpi/bootstrap/upload.js: -------------------------------------------------------------------------------- 1 | import { fp } from 'funpi'; 2 | import fastifyMultipart from '@fastify/multipart'; 3 | 4 | async function plugin(fastify) { 5 | await fastify.register(fastifyMultipart, { 6 | attachFieldsToBody: true, 7 | limits: { 8 | fieldNameSize: 100, 9 | fieldSize: 100, 10 | fields: 10, 11 | fileSize: 100000000, 12 | files: 1, 13 | headerPairs: 2000, 14 | parts: 1000 15 | } 16 | }); 17 | } 18 | 19 | export default fp(plugin, { 20 | name: 'upload' 21 | }); 22 | -------------------------------------------------------------------------------- /packages/funpi/bootstrap/xmlParse.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import { XMLParser, XMLValidator } from 'fast-xml-parser'; 3 | 4 | async function plugin(fastify) { 5 | const opts = { 6 | contentType: ['text/xml', 'application/xml', 'application/rss+xml'], 7 | validate: false 8 | }; 9 | 10 | function contentParser(req, payload, done) { 11 | const xmlParser = new XMLParser(opts); 12 | const parsingOpts = opts; 13 | 14 | let body = ''; 15 | payload.on('error', errorListener); 16 | payload.on('data', dataListener); 17 | payload.on('end', endListener); 18 | 19 | function errorListener(err) { 20 | done(err); 21 | } 22 | function endListener() { 23 | if (parsingOpts.validate) { 24 | const result = XMLValidator.validate(body, parsingOpts); 25 | if (result.err) { 26 | const invalidFormat = new Error('Invalid Format: ' + result.err.msg); 27 | invalidFormat.statusCode = 400; 28 | payload.removeListener('error', errorListener); 29 | payload.removeListener('data', dataListener); 30 | payload.removeListener('end', endListener); 31 | done(invalidFormat); 32 | } else { 33 | handleParseXml(body); 34 | } 35 | } else { 36 | handleParseXml(body); 37 | } 38 | } 39 | function dataListener(data) { 40 | body = body + data; 41 | } 42 | function handleParseXml(body) { 43 | try { 44 | done(null, xmlParser.parse(body)); 45 | } catch (err) { 46 | done(err); 47 | } 48 | } 49 | } 50 | 51 | fastify.addContentTypeParser(opts.contentType, contentParser); 52 | } 53 | 54 | export default fp(plugin, { 55 | name: 'funpiXmlParse' 56 | }); 57 | -------------------------------------------------------------------------------- /packages/funpi/config/env.js: -------------------------------------------------------------------------------- 1 | export const envConfig = { 2 | // 项目模式 3 | NODE_ENV: process.env.NODE_ENV, 4 | // 应用名称 5 | APP_NAME: process.env.APP_NAME, 6 | // 加密盐 7 | MD5_SALT: process.env.MD5_SALT, 8 | // 监听端口 9 | APP_PORT: Number(process.env.APP_PORT), 10 | // 监听主机 11 | LISTEN_HOST: process.env.LISTEN_HOST, 12 | // 超级管理员密码 13 | DEV_PASSWORD: process.env.DEV_PASSWORD, 14 | // 请求体大小 10M 15 | BODY_LIMIT: Number(process.env.BODY_LIMIT), 16 | // 是否进行参数验证 17 | PARAMS_CHECK: process.env.PARAMS_CHECK, 18 | // 日志等级 19 | LOG_LEVEL: process.env.LOG_LEVEL, 20 | // 数据库表主键方案 21 | TABLE_PRIMARY_KEY: process.env.TABLE_PRIMARY_KEY, 22 | // 时区 23 | TIMEZONE: process.env.TIMEZONE, 24 | // 数据库配置 25 | MYSQL_HOST: process.env.MYSQL_HOST, 26 | MYSQL_PORT: Number(process.env.MYSQL_PORT), 27 | MYSQL_DB: process.env.MYSQL_DB, 28 | MYSQL_USERNAME: process.env.MYSQL_USERNAME, 29 | MYSQL_PASSWORD: process.env.MYSQL_PASSWORD, 30 | // Redis配置 31 | REDIS_HOST: process.env.REDIS_HOST, 32 | REDIS_PORT: Number(process.env.REDIS_PORT), 33 | REDIS_USERNAME: process.env.REDIS_USERNAME, 34 | REDIS_PASSWORD: process.env.REDIS_PASSWORD, 35 | REDIS_DB: Number(process.env.REDIS_DB), 36 | REDIS_KEY_PREFIX: process.env.REDIS_KEY_PREFIX, 37 | // JWT配置 38 | JWT_SECRET: process.env.JWT_SECRET, 39 | JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN, 40 | JWT_ALGORITHM: process.env.JWT_ALGORITHM, 41 | // 邮件配置 42 | MAIL_HOST: process.env.MAIL_HOST, 43 | MAIL_PORT: Number(process.env.MAIL_PORT), 44 | MAIL_POOL: process.env.MAIL_POOL, 45 | MAIL_SECURE: process.env.MAIL_SECURE, 46 | MAIL_USER: process.env.MAIL_USER, 47 | MAIL_PASS: process.env.MAIL_PASS, 48 | MAIL_SENDER: process.env.MAIL_SENDER, 49 | MAIL_ADDRESS: process.env.MAIL_ADDRESS 50 | }; 51 | -------------------------------------------------------------------------------- /packages/funpi/config/http.js: -------------------------------------------------------------------------------- 1 | export const httpConfig = { 2 | SUCCESS: { symbol: 'SUCCESS', code: 0, msg: '操作成功' }, 3 | INSERT_SUCCESS: { symbol: 'INSERT_SUCCESS', code: 0, msg: '添加成功' }, 4 | SELECT_SUCCESS: { symbol: 'SELECT_SUCCESS', code: 0, msg: '查询成功' }, 5 | UPDATE_SUCCESS: { symbol: 'UPDATE_SUCCESS', code: 0, msg: '更新成功' }, 6 | DELETE_SUCCESS: { symbol: 'DELETE_SUCCESS', code: 0, msg: '删除成功' }, 7 | FAIL: { symbol: 'FAIL', code: 1, msg: '操作失败' }, 8 | INSERT_FAIL: { symbol: 'INSERT_FAIL', code: 1, msg: '添加失败' }, 9 | SELECT_FAIL: { symbol: 'SELECT_FAIL', code: 1, msg: '查询失败' }, 10 | UPDATE_FAIL: { symbol: 'UPDATE_FAIL', code: 1, msg: '更新失败' }, 11 | DELETE_FAIL: { symbol: 'DELETE_FAIL', code: 1, msg: '删除失败' }, 12 | INFO: { symbol: 'INFO', code: 11, msg: '提示' }, 13 | WARN: { symbol: 'WARN', code: 12, msg: '警告' }, 14 | ERROR: { symbol: 'ERROR', code: 13, msg: '错误' }, 15 | NOT_LOGIN: { symbol: 'NOT_LOGIN', code: 14, msg: '未登录' }, 16 | API_DISABLED: { symbol: 'API_DISABLED', code: 15, msg: '接口已禁用' }, 17 | NO_FILE: { symbol: 'NO_FILE', code: 17, msg: '文件不存在' }, 18 | NO_API: { symbol: 'NO_API', code: 18, msg: '接口不存在' }, 19 | NO_USER: { symbol: 'NO_USER', code: 19, msg: '用户不存在' }, 20 | NO_DATA: { symbol: 'NO_DATA', code: 20, msg: '数据不存在' }, 21 | NO_PERMISSION: { symbol: 'NO_PERMISSION', code: 21, msg: '无操作权限' }, 22 | PARAMS_SIGN_FAIL: { symbol: 'PARAMS_SIGN_FAIL', code: 22, msg: '参数签名错误' } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/funpi/config/menu.js: -------------------------------------------------------------------------------- 1 | export const menuConfig = [ 2 | { 3 | path: '/admin', 4 | name: '管理数据', 5 | children: [ 6 | { 7 | path: '/internal/admin', 8 | name: '管理员' 9 | } 10 | ] 11 | }, 12 | { 13 | path: '/dict', 14 | name: '字典管理', 15 | children: [ 16 | { 17 | path: '/internal/dict-category', 18 | name: '字典分类' 19 | }, 20 | { 21 | path: '/internal/dict', 22 | name: '字典列表' 23 | } 24 | ] 25 | }, 26 | { 27 | path: '/log', 28 | name: '日志数据', 29 | children: [ 30 | { 31 | path: '/internal/admin-login-log', 32 | name: '登录日志' 33 | }, 34 | { 35 | path: '/internal/admin-action-log', 36 | name: '操作日志' 37 | }, 38 | { 39 | path: '/internal/mail-log', 40 | name: '邮件日志' 41 | } 42 | ] 43 | }, 44 | { 45 | path: '/permission', 46 | name: '权限数据', 47 | children: [ 48 | { 49 | path: '/internal/menu', 50 | name: '菜单列表' 51 | }, 52 | { 53 | path: '/internal/api', 54 | name: '接口列表' 55 | }, 56 | { 57 | path: '/internal/role', 58 | name: '角色管理' 59 | } 60 | ] 61 | } 62 | ]; 63 | -------------------------------------------------------------------------------- /packages/funpi/config/path.js: -------------------------------------------------------------------------------- 1 | import { dirname, resolve, normalize } from 'pathe'; 2 | export const appDir = normalize(process.cwd()); 3 | export const funpiDir = dirname(dirname(import.meta.filename)); 4 | -------------------------------------------------------------------------------- /packages/funpi/config/role.js: -------------------------------------------------------------------------------- 1 | export const roleConfig = { 2 | visitor: { 3 | name: '游客', 4 | sort: 4, 5 | describe: '具备有限的权限和有限的查看内容' 6 | }, 7 | user: { 8 | name: '用户', 9 | sort: 3, 10 | describe: '用户权限和对于的内容查看' 11 | }, 12 | admin: { 13 | name: '管理', 14 | sort: 2, 15 | describe: '管理权限、除开发相关权限之外的权限等' 16 | }, 17 | super: { 18 | name: '超级管理', 19 | sort: 1, 20 | describe: '超级管理权限、除开发相关权限之外的权限等' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /packages/funpi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "funpi", 3 | "version": "7.20.27", 4 | "description": "FunPi(放屁) - 像放屁一样简单又自然的Node.js接口开发框架", 5 | "type": "module", 6 | "private": false, 7 | "publishConfig": { 8 | "access": "public", 9 | "registry": "https://registry.npmjs.org" 10 | }, 11 | "exports": { 12 | ".": "./funpi.js", 13 | "./scripts/*": "./scripts/*.js" 14 | }, 15 | "keywords": [ 16 | "fastify", 17 | "nodejs", 18 | "api" 19 | ], 20 | "files": [ 21 | "apis/", 22 | "bootstrap/", 23 | "config/", 24 | "plugins/", 25 | "schema/", 26 | "scripts/", 27 | "tables/", 28 | "utils/", 29 | ".gitignore", 30 | ".npmrc", 31 | ".prettierignore", 32 | ".prettier", 33 | "funpi.js", 34 | "LICENSE", 35 | "package.json", 36 | "README.md" 37 | ], 38 | "engines": { 39 | "node": ">=20.6.0" 40 | }, 41 | "author": "chensuiyi ", 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/chenbimo/funpi" 45 | }, 46 | "homepage": "https://chensuiyi.me", 47 | "dependencies": { 48 | "@fastify/autoload": "^6.3.0", 49 | "@fastify/cors": "^11.0.1", 50 | "@fastify/jwt": "^9.1.0", 51 | "@fastify/multipart": "^9.0.3", 52 | "@fastify/static": "^8.1.1", 53 | "ajv": "^8.17.1", 54 | "bullmq": "^5.52.2", 55 | "es-toolkit": "^1.37.2", 56 | "fast-xml-parser": "^5.2.3", 57 | "fastify": "^5.3.3", 58 | "fastify-plugin": "^5.0.1", 59 | "knex": "^3.1.0", 60 | "mysql2": "^3.14.1", 61 | "nodemailer": "^7.0.3", 62 | "pathe": "^2.0.3", 63 | "picomatch": "^4.0.2", 64 | "redis": "^5.0.1", 65 | "safe-stable-stringify": "^2.5.0", 66 | "winston": "^3.17.0", 67 | "winston-daily-rotate-file": "^5.0.0" 68 | }, 69 | "gitHead": "1c28c0de7c0af8aa4582c45ab2d98e66c597c7a1" 70 | } 71 | -------------------------------------------------------------------------------- /packages/funpi/plugins/jwt.js: -------------------------------------------------------------------------------- 1 | // 外部模块 2 | import fp from 'fastify-plugin'; 3 | import fastifyJwt from '@fastify/jwt'; 4 | 5 | async function plugin(fastify) { 6 | await fastify.register(fastifyJwt, { 7 | secret: process.env.JWT_SECRET, 8 | decoratorName: 'session', 9 | decode: { 10 | complete: true 11 | }, 12 | sign: { 13 | algorithm: process.env.JWT_ALGORITHM, 14 | expiresIn: process.env.JWT_EXPIRES_IN 15 | }, 16 | verify: { 17 | algorithms: [process.env.JWT_ALGORITHM] 18 | } 19 | }); 20 | } 21 | 22 | export default fp(plugin, { name: 'jwt' }); 23 | -------------------------------------------------------------------------------- /packages/funpi/plugins/logger.js: -------------------------------------------------------------------------------- 1 | // 核心模块 2 | import { resolve } from 'pathe'; 3 | // 外部模块 4 | import winston from 'winston'; 5 | import 'winston-daily-rotate-file'; 6 | 7 | import { appDir } from '../config/path.js'; 8 | 9 | const fileConfig = { 10 | dirname: resolve(appDir, 'logs'), 11 | filename: '%DATE%.log', 12 | datePattern: 'YYYY-MM-DD', 13 | zippedArchive: false, 14 | maxSize: '50m' 15 | }; 16 | 17 | const fileTransport = new winston.transports.DailyRotateFile(fileConfig); 18 | 19 | const configParams = { 20 | levels: { 21 | fatal: 0, 22 | error: 1, 23 | warn: 2, 24 | info: 3, 25 | trace: 4, 26 | debug: 5 27 | }, 28 | level: process.env.LOG_LEVEL || 'warn', 29 | format: winston.format.combine( 30 | winston.format.timestamp({ 31 | format: () => { 32 | return new Date().toLocaleString('zh-CN', { 33 | timeZone: process.env.TIMEZONE, 34 | hour12: false, 35 | year: 'numeric', 36 | month: '2-digit', 37 | day: '2-digit', 38 | hour: '2-digit', 39 | minute: '2-digit', 40 | second: '2-digit' 41 | }); 42 | } 43 | }), 44 | winston.format.json() 45 | ), 46 | transports: [], 47 | exitOnError: false 48 | }; 49 | 50 | // 如果是产品环境,则将日志写到文件中 51 | // 如果是开发环境,则直接打印日志 52 | if (process.env.NODE_ENV === 'production') { 53 | configParams.transports = [fileTransport]; 54 | } else { 55 | configParams.transports = [new winston.transports.Console(), fileTransport]; 56 | } 57 | 58 | const logger = winston.createLogger(configParams); 59 | 60 | export default logger; 61 | -------------------------------------------------------------------------------- /packages/funpi/plugins/swagger.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import fastifySwagger from '@fastify/swagger'; 3 | import fastifySwaggerUi from '@fastify/swagger-ui'; 4 | 5 | async function plugin(fastify) { 6 | await fastify.register(fastifySwagger, { 7 | mode: 'dynamic', 8 | swagger: { 9 | info: { 10 | title: `${process.env.APP_NAME}接口文档`, 11 | description: `${process.env.APP_NAME}接口文档` 12 | }, 13 | host: '127.0.0.1', 14 | schemes: ['http'], 15 | consumes: ['application/json'], 16 | produces: ['application/json'] 17 | } 18 | }); 19 | 20 | await fastify.register(fastifySwaggerUi, { 21 | routePrefix: '/swagger', 22 | initOAuth: {}, 23 | uiConfig: { 24 | docExpansion: 'none', 25 | deepLinking: false 26 | }, 27 | staticCSP: true 28 | }); 29 | } 30 | export default fp(plugin, { 31 | name: 'swagger' 32 | }); 33 | -------------------------------------------------------------------------------- /packages/funpi/schema/menu.js: -------------------------------------------------------------------------------- 1 | export const menuSchema = { 2 | title: '菜单字段', 3 | type: 'array', 4 | items: { 5 | title: '主菜单', 6 | type: 'object', 7 | properties: { 8 | path: { 9 | title: '菜单路径', 10 | type: 'string', 11 | pattern: '^\\/[a-z][a-z0-9\\/-]*$' 12 | }, 13 | name: { 14 | title: '菜单名称', 15 | type: 'string' 16 | }, 17 | children: { 18 | title: '子菜单', 19 | type: 'array', 20 | items: { 21 | type: 'object', 22 | properties: { 23 | path: { 24 | title: '子菜单路径', 25 | type: 'string', 26 | pattern: '^\\/[a-z][a-z0-9\\/-]*$' 27 | }, 28 | name: { 29 | title: '菜单名称', 30 | type: 'string' 31 | } 32 | }, 33 | additionalProperties: false, 34 | required: ['path', 'name'] 35 | } 36 | } 37 | }, 38 | additionalProperties: false, 39 | required: ['path', 'name', 'children'] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /packages/funpi/tables/admin.js: -------------------------------------------------------------------------------- 1 | export const tableName = '系统管理员表'; 2 | export const tableData = { 3 | role: { 4 | name: '角色代号', 5 | type: 'string', 6 | default: '', 7 | min: 1, 8 | max: 50, 9 | pattern: '^[a-z][a-z0-9_-]*$' 10 | }, 11 | username: { 12 | name: '用户名', 13 | type: 'string', 14 | default: '', 15 | min: 1, 16 | max: 20, 17 | pattern: '^[a-z][a-zA-Z0-9_-]*$' 18 | }, 19 | password: { 20 | name: '密码', 21 | type: 'string', 22 | default: '', 23 | min: 6, 24 | max: 300 25 | }, 26 | nickname: { 27 | name: '昵称', 28 | type: 'string', 29 | default: '', 30 | min: 1, 31 | max: 30 32 | }, 33 | phone: { 34 | name: '手机号', 35 | type: 'string', 36 | default: '', 37 | min: 1, 38 | max: 30 39 | }, 40 | weixin: { 41 | name: '微信号', 42 | type: 'string', 43 | default: '', 44 | min: 6, 45 | max: 30, 46 | pattern: '^[a-zA-Z][-_a-zA-Z0-9]{5,30}$' 47 | }, 48 | qq: { 49 | name: 'QQ号', 50 | type: 'string', 51 | default: '', 52 | min: 5, 53 | max: 20, 54 | pattern: '^\\d{5,}$' 55 | }, 56 | email: { 57 | name: '邮箱', 58 | type: 'string', 59 | default: '', 60 | min: 5, 61 | max: 50, 62 | pattern: '^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$' 63 | }, 64 | avatar: { 65 | name: '头像', 66 | type: 'string', 67 | default: '', 68 | min: 0, 69 | max: 300 70 | }, 71 | is_system: { 72 | name: '是否系统数据(不可删除)', 73 | type: 'tinyInt', 74 | default: 0, 75 | enum: [0, 1] 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /packages/funpi/tables/adminActionLog.js: -------------------------------------------------------------------------------- 1 | export const tableName = '管理员操作日志表'; 2 | export const tableData = { 3 | user_id: { 4 | name: '登录用户ID', 5 | type: 'bigInt', 6 | default: 0, 7 | isIndex: true, 8 | min: 0 9 | }, 10 | username: { 11 | name: '用户名', 12 | type: 'string', 13 | default: '', 14 | max: 50 15 | }, 16 | nickname: { 17 | name: '昵称', 18 | type: 'string', 19 | default: '', 20 | max: 50 21 | }, 22 | role: { 23 | name: '角色', 24 | type: 'string', 25 | default: '', 26 | max: 50 27 | }, 28 | ip: { 29 | name: 'ip', 30 | type: 'string', 31 | default: '', 32 | max: 50 33 | }, 34 | ua: { 35 | name: 'ua', 36 | type: 'string', 37 | default: '', 38 | max: 500 39 | }, 40 | api: { 41 | name: '访问接口', 42 | type: 'string', 43 | default: '', 44 | max: 200 45 | }, 46 | params: { 47 | name: '请求参数', 48 | type: 'string', 49 | default: '', 50 | max: 5000 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /packages/funpi/tables/adminLoginLog.js: -------------------------------------------------------------------------------- 1 | export const tableName = '管理员登录日志表'; 2 | export const tableData = { 3 | user_id: { 4 | name: '登录用户ID', 5 | type: 'bigInt', 6 | default: 0, 7 | isIndex: true, 8 | min: 0 9 | }, 10 | username: { 11 | name: '用户名', 12 | type: 'string', 13 | default: '', 14 | max: 50 15 | }, 16 | nickname: { 17 | name: '昵称', 18 | type: 'string', 19 | default: '', 20 | max: 50 21 | }, 22 | role: { 23 | name: '角色', 24 | type: 'string', 25 | default: '', 26 | max: 50 27 | }, 28 | ip: { 29 | name: 'ip', 30 | type: 'string', 31 | default: '', 32 | max: 50 33 | }, 34 | ua: { 35 | name: 'ua', 36 | type: 'string', 37 | default: '', 38 | max: 500 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /packages/funpi/tables/api.js: -------------------------------------------------------------------------------- 1 | export const tableName = '系统接口表'; 2 | export const tableData = { 3 | pid: { 4 | name: '父级ID', 5 | type: 'bigInt', 6 | default: 0, 7 | isIndex: true, 8 | min: 1 9 | }, 10 | pids: { 11 | name: '父级ID链', 12 | type: 'string', 13 | default: '0', 14 | max: 1000 15 | }, 16 | name: { 17 | name: '名称', 18 | type: 'string', 19 | default: '', 20 | max: 100 21 | }, 22 | value: { 23 | name: '值', 24 | type: 'string', 25 | default: '', 26 | max: 500 27 | }, 28 | sort: { 29 | name: '字典排序', 30 | type: 'bigInt', 31 | default: 0, 32 | min: 0 33 | }, 34 | describe: { 35 | name: '描述', 36 | type: 'string', 37 | default: '', 38 | max: 500 39 | }, 40 | state: { 41 | name: '状态 0 正常 1 白名单 2 黑名单', 42 | type: 'int', 43 | default: 0, 44 | enum: [0, 1, 2] 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /packages/funpi/tables/dict.js: -------------------------------------------------------------------------------- 1 | export const tableName = '字典数据表'; 2 | export const tableData = { 3 | category_id: { 4 | name: '分类ID', 5 | type: 'bigInt', 6 | default: 0, 7 | isIndex: true, 8 | min: 0 9 | }, 10 | category_code: { 11 | name: '分类编码', 12 | type: 'string', 13 | default: '', 14 | isIndex: true, 15 | max: 50, 16 | pattern: '^[a-z][a-z0-9_-]*$' 17 | }, 18 | code: { 19 | name: '字典编码', 20 | type: 'string', 21 | default: '', 22 | max: 50, 23 | pattern: '^[a-z][a-z0-9_-]*$' 24 | }, 25 | name: { 26 | name: '字典名称', 27 | type: 'string', 28 | default: '', 29 | max: 100 30 | }, 31 | value: { 32 | name: '字典值', 33 | type: 'string', 34 | default: '', 35 | max: 1000 36 | }, 37 | symbol: { 38 | name: '数字类型或字符串类型', 39 | type: 'string', 40 | default: '', 41 | enum: ['string', 'number'] 42 | }, 43 | sort: { 44 | name: '字典排序', 45 | type: 'bigInt', 46 | default: 0, 47 | min: 0 48 | }, 49 | describe: { 50 | name: '描述', 51 | type: 'string', 52 | default: '', 53 | max: 500 54 | }, 55 | thumbnail: { 56 | name: '缩略图', 57 | type: 'string', 58 | default: '', 59 | max: 500 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /packages/funpi/tables/dictCategory.js: -------------------------------------------------------------------------------- 1 | export const tableName = '字典分类表'; 2 | export const tableData = { 3 | code: { 4 | name: '字典分类编码', 5 | type: 'string', 6 | default: '', 7 | min: 1, 8 | max: 50, 9 | pattern: '^[a-z][a-z0-9_-]*$' 10 | }, 11 | name: { 12 | name: '字典分类名称', 13 | type: 'string', 14 | default: '', 15 | min: 1, 16 | max: 100 17 | }, 18 | sort: { 19 | name: '字典分类排序', 20 | type: 'bigInt', 21 | default: 0, 22 | min: 0 23 | }, 24 | describe: { 25 | name: '描述', 26 | type: 'string', 27 | default: '', 28 | min: 0, 29 | max: 500 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /packages/funpi/tables/mailLog.js: -------------------------------------------------------------------------------- 1 | export const tableName = '邮件日志表'; 2 | export const tableData = { 3 | login_email: { 4 | type: 'string', 5 | name: '登录邮箱', 6 | default: '', 7 | max: 100 8 | }, 9 | from_name: { 10 | type: 'string', 11 | name: '发送者昵称', 12 | default: '', 13 | max: 100 14 | }, 15 | from_email: { 16 | type: 'string', 17 | name: '发送者邮箱', 18 | default: '', 19 | max: 100 20 | }, 21 | to_email: { 22 | name: '接收者邮箱', 23 | type: 'string', 24 | default: '', 25 | max: 5000 26 | }, 27 | email_type: { 28 | name: '邮件类型', 29 | type: 'string', 30 | default: 'common', 31 | min: 1, 32 | max: 100 33 | }, 34 | email_code: { 35 | name: '邮件识别码', 36 | type: 'string', 37 | min: 1, 38 | max: 100, 39 | default: '' 40 | }, 41 | text_content: { 42 | name: '发送内容', 43 | type: 'string', 44 | default: '', 45 | max: 10000 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /packages/funpi/tables/menu.js: -------------------------------------------------------------------------------- 1 | export const tableName = '系统菜单表'; 2 | export const tableData = { 3 | pid: { 4 | name: '父级ID', 5 | type: 'bigInt', 6 | default: 0, 7 | isIndex: true, 8 | min: 0 9 | }, 10 | image: { 11 | name: '菜单图标', 12 | type: 'string', 13 | default: '', 14 | max: 500 15 | }, 16 | name: { 17 | name: '名称', 18 | type: 'string', 19 | default: '', 20 | max: 100 21 | }, 22 | value: { 23 | name: '路由', 24 | type: 'string', 25 | default: '', 26 | isUnique: true, 27 | max: 500 28 | }, 29 | sort: { 30 | name: '字典排序', 31 | type: 'bigInt', 32 | default: 100, 33 | min: 100 34 | }, 35 | describe: { 36 | name: '描述', 37 | type: 'string', 38 | default: '', 39 | max: 500 40 | }, 41 | is_open: { 42 | name: '是否公开', 43 | type: 'tinyInt', 44 | default: 0, 45 | enum: [0, 1] 46 | }, 47 | is_system: { 48 | name: '是否系统账号(不可删除)', 49 | type: 'tinyInt', 50 | default: 0, 51 | enum: [0, 1] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /packages/funpi/tables/role.js: -------------------------------------------------------------------------------- 1 | export const tableName = '系统角色表'; 2 | export const tableData = { 3 | code: { 4 | name: '角色编码', 5 | type: 'string', 6 | default: '', 7 | max: 50, 8 | isIndex: true, 9 | pattern: '^[a-z][a-z0-9_]*$' 10 | }, 11 | name: { 12 | name: '角色名称', 13 | type: 'string', 14 | default: '', 15 | max: 100 16 | }, 17 | describe: { 18 | name: '角色描述', 19 | type: 'string', 20 | default: '', 21 | max: 500 22 | }, 23 | menu_ids: { 24 | name: '角色菜单', 25 | type: 'mediumText', 26 | max: 50000 27 | }, 28 | api_ids: { 29 | name: '角色接口', 30 | type: 'mediumText', 31 | max: 50000 32 | }, 33 | sort: { 34 | name: '角色排序', 35 | type: 'bigInt', 36 | default: 1, 37 | min: 1 38 | }, 39 | is_system: { 40 | name: '是否系统角色(不可删除)', 41 | type: 'tinyInt', 42 | default: 0, 43 | enum: [0, 1] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /packages/funpi/todo.md: -------------------------------------------------------------------------------- 1 | ## 待办 2 | 3 | - [ ] 限制图片上传大小 4 | - [ ] 管理上传的图片 5 | - [ ] 同步数据库结构的时候,判断为必须停服维护才可以 6 | - [ ] 处理异步通知问题,可能需要用到消息队列 7 | - [ ] 添加和更新菜单后,管理员可以可以立马看到效果 8 | - [ ] 图片作为日志打印了 9 | - [ ] 图片上传buffer警告问题,写法更新 10 | - [x] 没有改动的系统表不要更新 11 | - [ ] fnInreUUID函数优化为更稳定的版本 12 | - [x] 把syncDatabase函数从内核移出 13 | - [x] 局域网访问 14 | - [ ] 定时还原数据库功能 15 | - [ ] 单点登录 16 | - [ ] jwt 放到redis中,可以被主动踢出功能 17 | - [ ] 拉黑功能 18 | - [ ] 初始化数据库自增模式后,禁止再次改变 19 | - [ ] 支付订单支持优惠码(直接减免),折扣码(百分比)减免 20 | - [x] 系统接口和用户接口重复的判断 21 | - [ ] 增加对控制公众号相关的功能 22 | - [ ] 增加对跨域的配置支持 23 | - [ ] 增加一个配置可视化面板 24 | - [ ] 给接口做隐藏验证,如果不是官方域名的请求,则不予返回数据。 25 | - [ ] 增加监控报错接口和功能 26 | - [ ] 限速要排除微信回调接口 27 | - [ ] 实现在线人数等功能 28 | - [ ] 数据库同步字段名称改变不要删除,把该字段改成其他名称。 29 | - [ ] 提供数据库创建和对比,不提供同步功能。 30 | - [ ] 限制Node.js版本只能为v20 31 | - [ ] 判断process.env.NODE_ENV是否为production或development 32 | - [x] 判断系统接口和用户接口名称冲突问题 33 | - [ ] 缓存使用配置文件统一管理 34 | - [ ] 每个接口可以限制请求来源,IP等 35 | -------------------------------------------------------------------------------- /packages/funpi/utils/check.js: -------------------------------------------------------------------------------- 1 | // 内核模块 2 | import { resolve, basename } from 'pathe'; 3 | import { existsSync, mkdirSync, readdirSync } from 'node:fs'; 4 | // 外部模块 5 | import Ajv from 'ajv'; 6 | 7 | // 内部模块 8 | 9 | // 配置文件 10 | import { appDir, funpiDir } from '../config/path.js'; 11 | 12 | import { envConfig } from '../config/env.js'; 13 | import { menuConfig as internalMenuConfig } from '../config/menu.js'; 14 | // 协议配置 15 | import { envSchema } from '../schema/env.js'; 16 | import { menuSchema } from '../schema/menu.js'; 17 | // 工具函数 18 | import { fnImport, log4state, fnIsCamelCase, fnApiFiles, fnApiFilesCheck } from './index.js'; 19 | import { ajvZh } from './ajvZh.js'; 20 | 21 | export const initCheck = async (fastify) => { 22 | // 判断运行目录下是否有 funpi.js 文件 23 | if (existsSync(resolve(appDir, 'funpi.js')) === false) { 24 | console.log(`${log4state('warn')} 请在 funpi 项目根目录下运行`); 25 | process.exit(); 26 | } 27 | 28 | // 确保关键目录存在 ================================================== 29 | if (existsSync(resolve(appDir, 'addons')) === false) { 30 | mkdirSync(resolve(appDir, 'addons')); 31 | } 32 | if (existsSync(resolve(appDir, 'apis')) === false) { 33 | mkdirSync(resolve(appDir, 'apis')); 34 | } 35 | if (existsSync(resolve(appDir, 'config')) === false) { 36 | mkdirSync(resolve(appDir, 'config')); 37 | } 38 | if (existsSync(resolve(appDir, 'tables')) === false) { 39 | mkdirSync(resolve(appDir, 'tables')); 40 | } 41 | if (existsSync(resolve(appDir, 'plugins')) === false) { 42 | mkdirSync(resolve(appDir, 'plugins')); 43 | } 44 | if (existsSync(resolve(appDir, 'logs')) === false) { 45 | mkdirSync(resolve(appDir, 'logs')); 46 | } 47 | if (existsSync(resolve(appDir, 'public')) === false) { 48 | mkdirSync(resolve(appDir, 'public')); 49 | } 50 | 51 | const ajv = new Ajv({ 52 | strict: false, 53 | allErrors: true, 54 | verbose: false 55 | }); 56 | 57 | // 验证配置文件 ================================================== 58 | const validEnvResult = ajv.validate(envSchema, envConfig); 59 | if (!validEnvResult) { 60 | ajvZh(ajv.errors); 61 | console.log(log4state('error'), '[ 环境变量错误 ] \n' + ajv.errorsText(ajv.errors, { separator: '\n' })); 62 | process.exit(); 63 | } 64 | 65 | // 验证菜单配置 66 | const { menuConfig: userMenuConfig } = await fnImport(resolve(appDir, 'config', 'menu.js'), 'menuConfig', {}); 67 | const allMenuConfig = [...userMenuConfig, ...internalMenuConfig]; 68 | const validMenuResult = ajv.validate(menuSchema, allMenuConfig); 69 | if (!validMenuResult) { 70 | ajvZh(ajv.errors); 71 | console.log(log4state('error'), '[ 菜单配置错误 ] \n' + ajv.errorsText(ajv.errors, { separator: '\n' })); 72 | process.exit(); 73 | } 74 | 75 | const menuPaths = new Set(); 76 | 77 | for (const menu of allMenuConfig) { 78 | // 检查主菜单路径 79 | if (menuPaths.has(menu.path)) { 80 | console.log(log4state('error'), '[ 重复的菜单路径 ] ' + menu.path); 81 | process.exit(); 82 | } 83 | menuPaths.add(menu.path); 84 | 85 | // 检查子菜单路径 86 | for (const child of menu.children) { 87 | if (menuPaths.has(child.path)) { 88 | console.log(log4state('error'), '[ 重复的菜单路径 ] ' + child.path); 89 | process.exit(); 90 | } 91 | menuPaths.add(child.path); 92 | } 93 | } 94 | 95 | // 接口检测 96 | await fnApiFilesCheck(); 97 | 98 | // ================================================== 99 | 100 | // 启动前验证 101 | if (process.env.MD5_SALT === 'funpi123456') { 102 | console.log(`${log4state('warn')} 请修改默认加密盐值!环境变量:MD5_SALT`); 103 | } 104 | 105 | if (process.env.DEV_PASSWORD === 'funpi123456') { 106 | console.log(`${log4state('warn')} 请修改开发管理员密码!环境变量:DEV_PASSWORD`); 107 | } 108 | 109 | if (process.env.JWT_SECRET === 'funpi123456') { 110 | console.log(`${log4state('warn')} 请修改 JWT 密钥!环境变量:JWT_SECRET`); 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /packages/funpi/utils/colors.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2024 Oleksii Raspopov, Kostiantyn Denysov, Anton Verinov 2 | 3 | let p = process || {}, 4 | argv = p.argv || [], 5 | env = p.env || {}; 6 | let isColorSupported = !(!!env.NO_COLOR || argv.includes('--no-color')) && (!!env.FORCE_COLOR || argv.includes('--color') || p.platform === 'win32' || ((p.stdout || {}).isTTY && env.TERM !== 'dumb') || !!env.CI); 7 | 8 | let formatter = 9 | (open, close, replace = open) => 10 | (input) => { 11 | let string = '' + input, 12 | index = string.indexOf(close, open.length); 13 | return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close; 14 | }; 15 | 16 | let replaceClose = (string, close, replace, index) => { 17 | let result = '', 18 | cursor = 0; 19 | do { 20 | result += string.substring(cursor, index) + replace; 21 | cursor = index + close.length; 22 | index = string.indexOf(close, cursor); 23 | } while (~index); 24 | return result + string.substring(cursor); 25 | }; 26 | 27 | let createColors = (enabled = isColorSupported) => { 28 | let f = enabled ? formatter : () => String; 29 | return { 30 | // 符号 31 | 32 | isColorSupported: enabled, 33 | reset: f('\x1b[0m', '\x1b[0m'), 34 | bold: f('\x1b[1m', '\x1b[22m', '\x1b[22m\x1b[1m'), 35 | dim: f('\x1b[2m', '\x1b[22m', '\x1b[22m\x1b[2m'), 36 | italic: f('\x1b[3m', '\x1b[23m'), 37 | underline: f('\x1b[4m', '\x1b[24m'), 38 | inverse: f('\x1b[7m', '\x1b[27m'), 39 | hidden: f('\x1b[8m', '\x1b[28m'), 40 | strikethrough: f('\x1b[9m', '\x1b[29m'), 41 | 42 | black: f('\x1b[30m', '\x1b[39m'), 43 | red: f('\x1b[31m', '\x1b[39m'), 44 | green: f('\x1b[32m', '\x1b[39m'), 45 | yellow: f('\x1b[33m', '\x1b[39m'), 46 | blue: f('\x1b[34m', '\x1b[39m'), 47 | magenta: f('\x1b[35m', '\x1b[39m'), 48 | cyan: f('\x1b[36m', '\x1b[39m'), 49 | white: f('\x1b[37m', '\x1b[39m'), 50 | gray: f('\x1b[90m', '\x1b[39m'), 51 | 52 | bgBlack: f('\x1b[40m', '\x1b[49m'), 53 | bgRed: f('\x1b[41m', '\x1b[49m'), 54 | bgGreen: f('\x1b[42m', '\x1b[49m'), 55 | bgYellow: f('\x1b[43m', '\x1b[49m'), 56 | bgBlue: f('\x1b[44m', '\x1b[49m'), 57 | bgMagenta: f('\x1b[45m', '\x1b[49m'), 58 | bgCyan: f('\x1b[46m', '\x1b[49m'), 59 | bgWhite: f('\x1b[47m', '\x1b[49m'), 60 | 61 | blackBright: f('\x1b[90m', '\x1b[39m'), 62 | redBright: f('\x1b[91m', '\x1b[39m'), 63 | greenBright: f('\x1b[92m', '\x1b[39m'), 64 | yellowBright: f('\x1b[93m', '\x1b[39m'), 65 | blueBright: f('\x1b[94m', '\x1b[39m'), 66 | magentaBright: f('\x1b[95m', '\x1b[39m'), 67 | cyanBright: f('\x1b[96m', '\x1b[39m'), 68 | whiteBright: f('\x1b[97m', '\x1b[39m'), 69 | 70 | bgBlackBright: f('\x1b[100m', '\x1b[49m'), 71 | bgRedBright: f('\x1b[101m', '\x1b[49m'), 72 | bgGreenBright: f('\x1b[102m', '\x1b[49m'), 73 | bgYellowBright: f('\x1b[103m', '\x1b[49m'), 74 | bgBlueBright: f('\x1b[104m', '\x1b[49m'), 75 | bgMagentaBright: f('\x1b[105m', '\x1b[49m'), 76 | bgCyanBright: f('\x1b[106m', '\x1b[49m'), 77 | bgWhiteBright: f('\x1b[107m', '\x1b[49m') 78 | }; 79 | }; 80 | 81 | const colors = createColors(); 82 | 83 | export { colors }; 84 | -------------------------------------------------------------------------------- /packages/vscode/README.md: -------------------------------------------------------------------------------- 1 | # funpi 是什么? 2 | 3 | 中文名称 `放屁` 接口框架。 4 | 5 | 像放屁一样简单又自然的 `Node.js` 接口开发框架。 6 | 7 | > 注意:本项目 `v6` 版本为自用,`v7` 为内测版,`v8` 才是公测版。 8 | 9 | ### 仓库地址 10 | 11 | [github - https://github.com/chenbimo/funpi](https://github.com/chenbimo/funpi) 12 | 13 | ### 作者介绍 14 | 15 | [前端之虎陈随易 https://chensuiyi.me](https://chensuiyi.me) 16 | 17 | ### 文档教程 18 | 19 | 请到 [前端之虎陈随易 https://chensuiyi.me](https://chensuiyi.me) 网站查看。 20 | -------------------------------------------------------------------------------- /packages/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@funpi/vscode", 3 | "version": "7.20.27", 4 | "description": "FunPi(放屁) - VSCode插件占位", 5 | "type": "module", 6 | "private": false, 7 | "publishConfig": { 8 | "access": "public", 9 | "registry": "https://registry.npmjs.org" 10 | }, 11 | "exports": {}, 12 | "keywords": [ 13 | "fastify", 14 | "nodejs", 15 | "api" 16 | ], 17 | "files": [ 18 | "LICENSE", 19 | "package.json", 20 | "README.md" 21 | ], 22 | "engines": { 23 | "node": ">=20.6.0" 24 | }, 25 | "author": "chensuiyi ", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/chenbimo/funpi" 29 | }, 30 | "homepage": "https://chensuiyi.me", 31 | "gitHead": "1c28c0de7c0af8aa4582c45ab2d98e66c597c7a1" 32 | } 33 | --------------------------------------------------------------------------------