feat: 公共表更名 common/member 前缀 + 插件命令自动加载 + 版本 1.1.1
- 18 张公共表更名(addon/attachment/configure/links/spider_log/spider_stat/sms/notice/ad/task/prop/medal/help/card -> common_*,member_wallets->member_wallet,score_rule/score_log -> member_*,addon_config->common_addonconf),模型全部对齐新表名,Db 直引用清零 - Attachment 模型补 表名绑定,修复富文本上传查 wxapp_attachment 1146 隐患 - install.sql + 迁移 SQL:backend_admin/backend_role delete_at 默认 0,修复软删除(NULL != 0)误过滤导致后台菜单为空 - AppService::boot() 支持插件 info.php 声明 commands 自动注册插件命令(psr-4 自动加载,坏类名自动跳过) - 各插件(haonav/mqttbroker/wxchat/articles/blog/forum 等)字段与配置同步调整 - 框架版本 1.1.0 -> 1.1.1
This commit is contained in:
@@ -0,0 +1,597 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "YwxApp 标准 App 接口 v1",
|
||||
"description": "YwxApp App/小程序/第三方客户端标准接口。\n\n统一响应:{\"code\":0,\"msg\":\"success\",\"data\":{},\"timestamp\":1765900000}\n- code=0 成功;code=1 业务失败;code=401 未登录/鉴权失败\n- 业务 code 与 HTTP 状态码解耦(未登录时 HTTP 状态也为 401)\n- 需登录接口在 Header 携带 Authorization: Bearer <access_token>\n- 登录类接口 data 内含 access_token / refresh_token / expires_in;响应顶层也回写 token 字段",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{ "url": "http://localhost/api/v1", "description": "本地开发" }
|
||||
],
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"bearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"ApiResponse": {
|
||||
"type": "object",
|
||||
"required": ["code", "msg", "timestamp"],
|
||||
"properties": {
|
||||
"code": { "type": "integer", "description": "0=成功 1=业务失败 401=未登录", "example": 0 },
|
||||
"msg": { "type": "string", "description": "提示信息", "example": "success" },
|
||||
"data": { "type": "object", "nullable": true, "description": "业务数据" },
|
||||
"timestamp": { "type": "integer", "description": "服务器时间戳(秒)", "example": 1765900000 }
|
||||
}
|
||||
},
|
||||
"LoginData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uid": { "type": "integer", "example": 1 },
|
||||
"account": { "type": "string", "example": "testuser" },
|
||||
"nickname": { "type": "string", "example": "测试用户" },
|
||||
"mobile": { "type": "string", "example": "" },
|
||||
"email": { "type": "string", "example": "" },
|
||||
"avatar": { "type": "string", "example": "" },
|
||||
"status": { "type": "integer", "example": 1 },
|
||||
"access_token": { "type": "string", "example": "eyJ0eXAiOiJKV1Q..." },
|
||||
"refresh_token": { "type": "string", "example": "def50200..." },
|
||||
"expires_in": { "type": "integer", "example": 3600 },
|
||||
"token_type": { "type": "string", "example": "Bearer" }
|
||||
}
|
||||
},
|
||||
"ProfileData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uid": { "type": "integer", "example": 1 },
|
||||
"account": { "type": "string", "example": "testuser" },
|
||||
"nickname": { "type": "string", "example": "测试用户" },
|
||||
"mobile": { "type": "string", "example": "" },
|
||||
"email": { "type": "string", "example": "" },
|
||||
"avatar": { "type": "string", "example": "" },
|
||||
"status": { "type": "integer", "example": 1 },
|
||||
"status_text": { "type": "string", "example": "正常" },
|
||||
"last_login": { "type": "string", "example": "2026-08-17 10:00:00" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [{ "bearerAuth": [] }],
|
||||
"paths": {
|
||||
"/common/init": {
|
||||
"get": {
|
||||
"tags": ["Common"],
|
||||
"summary": "启动初始化",
|
||||
"description": "App 冷启动首屏调用,返回站点信息/注册方式/版本。公开。",
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": {
|
||||
"code": 0, "msg": "success",
|
||||
"data": { "site": {}, "register_type": "all", "version": "1.0.0" },
|
||||
"timestamp": 1765900000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/common/config": {
|
||||
"get": {
|
||||
"tags": ["Common"],
|
||||
"summary": "站点配置",
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "success", "data": { "site": {} }, "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/common/upload": {
|
||||
"post": {
|
||||
"tags": ["Common"],
|
||||
"summary": "文件上传(需登录)",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": { "file": { "type": "string", "format": "binary" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": {
|
||||
"code": 0, "msg": "上传成功",
|
||||
"data": { "url": "/storage/uploads/xxx.png", "path": "uploads/xxx.png" },
|
||||
"timestamp": 1765900000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/register": {
|
||||
"post": {
|
||||
"tags": ["User"],
|
||||
"summary": "注册",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["account", "password"],
|
||||
"properties": {
|
||||
"account": { "type": "string", "example": "testuser" },
|
||||
"password": { "type": "string", "example": "123456", "description": "至少6位" },
|
||||
"nickname": { "type": "string", "example": "测试用户" },
|
||||
"mobile": { "type": "string", "example": "13800138000" },
|
||||
"email": { "type": "string", "example": "user@example.com" },
|
||||
"captcha": { "type": "string", "example": "1234", "description": "手机号注册时必填" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "注册成功并签发 token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": {
|
||||
"code": 0, "msg": "注册成功",
|
||||
"data": { "uid": 1, "account": "testuser", "access_token": "eyJ...", "refresh_token": "def...", "expires_in": 3600, "token_type": "Bearer" },
|
||||
"access_token": "eyJ...", "access_expired": 3600, "refresh_token": "def...", "refresh_expired": 86400,
|
||||
"timestamp": 1765900000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/login": {
|
||||
"post": {
|
||||
"tags": ["User"],
|
||||
"summary": "账号密码登录",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["account", "password"],
|
||||
"properties": {
|
||||
"account": { "type": "string", "example": "testuser" },
|
||||
"password": { "type": "string", "example": "123456" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "登录成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": {
|
||||
"code": 0, "msg": "登录成功",
|
||||
"data": { "uid": 1, "access_token": "eyJ...", "refresh_token": "def...", "expires_in": 3600 },
|
||||
"timestamp": 1765900000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/loginBySms": {
|
||||
"post": {
|
||||
"tags": ["User"],
|
||||
"summary": "短信验证码登录(未注册自动注册)",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["mobile", "captcha"],
|
||||
"properties": {
|
||||
"mobile": { "type": "string", "example": "13800138000" },
|
||||
"captcha": { "type": "string", "example": "1234" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "登录成功", "data": { "uid": 1, "access_token": "eyJ...", "refresh_token": "def..." }, "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/refresh": {
|
||||
"post": {
|
||||
"tags": ["User"],
|
||||
"summary": "刷新 access_token",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["refresh_token"],
|
||||
"properties": { "refresh_token": { "type": "string", "example": "def50200..." } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "刷新成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "刷新成功", "data": { "access_token": "eyJ...", "refresh_token": "def...", "expires_in": 3600 }, "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "refresh_token 失效",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 401, "msg": "refresh_token 无效或已过期", "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/logout": {
|
||||
"post": {
|
||||
"tags": ["User"],
|
||||
"summary": "退出登录(需登录)",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "已登出", "data": [], "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/profile": {
|
||||
"get": {
|
||||
"tags": ["User"],
|
||||
"summary": "获取会员资料(需登录)",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "success", "data": { "uid": 1, "account": "testuser", "nickname": "测试", "status": 1, "status_text": "正常", "last_login": "2026-08-17 10:00:00" }, "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未登录",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 401, "msg": "请先登录", "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/updateProfile": {
|
||||
"post": {
|
||||
"tags": ["User"],
|
||||
"summary": "修改资料(需登录)",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nickname": { "type": "string", "example": "新昵称" },
|
||||
"mobile": { "type": "string", "example": "13800138000" },
|
||||
"email": { "type": "string", "example": "user@example.com" },
|
||||
"avatar": { "type": "string", "example": "" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "保存成功", "data": { "uid": 1, "nickname": "新昵称" }, "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/changePwd": {
|
||||
"post": {
|
||||
"tags": ["User"],
|
||||
"summary": "修改密码(需登录)",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["oldpassword", "newpassword"],
|
||||
"properties": {
|
||||
"oldpassword": { "type": "string", "example": "123456" },
|
||||
"newpassword": { "type": "string", "example": "654321", "description": "至少6位" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "密码修改成功", "data": [], "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/resetPwdBySms": {
|
||||
"post": {
|
||||
"tags": ["User"],
|
||||
"summary": "短信重置密码(免登录)",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["mobile", "captcha", "newpassword"],
|
||||
"properties": {
|
||||
"mobile": { "type": "string", "example": "13800138000" },
|
||||
"captcha": { "type": "string", "example": "1234" },
|
||||
"newpassword": { "type": "string", "example": "654321" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "密码重置成功", "data": [], "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/resetPwdByEmail": {
|
||||
"post": {
|
||||
"tags": ["User"],
|
||||
"summary": "邮箱重置密码(免登录)",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email", "captcha", "newpassword"],
|
||||
"properties": {
|
||||
"email": { "type": "string", "example": "user@example.com" },
|
||||
"captcha": { "type": "string", "example": "1234" },
|
||||
"newpassword": { "type": "string", "example": "654321" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "密码重置成功", "data": [], "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sms/send": {
|
||||
"post": {
|
||||
"tags": ["验证码"],
|
||||
"summary": "发送短信验证码",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["mobile", "event"],
|
||||
"properties": {
|
||||
"mobile": { "type": "string", "example": "13800138000" },
|
||||
"event": { "type": "string", "example": "register", "description": "register/login/mobilelogin/resetpwd/changepwd/changemobile" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "发送成功", "data": [], "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sms/check": {
|
||||
"post": {
|
||||
"tags": ["验证码"],
|
||||
"summary": "校验短信验证码",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["mobile", "event", "captcha"],
|
||||
"properties": {
|
||||
"mobile": { "type": "string", "example": "13800138000" },
|
||||
"event": { "type": "string", "example": "register" },
|
||||
"captcha": { "type": "string", "example": "1234" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "成功", "data": [], "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ems/send": {
|
||||
"post": {
|
||||
"tags": ["验证码"],
|
||||
"summary": "发送邮箱验证码",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email"],
|
||||
"properties": {
|
||||
"email": { "type": "string", "example": "user@example.com" },
|
||||
"event": { "type": "string", "example": "register" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "发送成功", "data": [], "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ems/check": {
|
||||
"post": {
|
||||
"tags": ["验证码"],
|
||||
"summary": "校验邮箱验证码",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email", "captcha"],
|
||||
"properties": {
|
||||
"email": { "type": "string", "example": "user@example.com" },
|
||||
"event": { "type": "string", "example": "register" },
|
||||
"captcha": { "type": "string", "example": "1234" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ApiResponse" },
|
||||
"example": { "code": 0, "msg": "验证码正确", "data": [], "timestamp": 1765900000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
# YwxApp 标准 App 接口 v1 测试文档
|
||||
|
||||
> 适用对象:App / 小程序 / 第三方客户端联调与测试(Apifox / Postman)
|
||||
> 基础路径:`/api/v1`(建议环境变量 `base_url = http://你的域名/api/v1`)
|
||||
> 最后更新:2026-08-17
|
||||
|
||||
---
|
||||
字段 值
|
||||
uid 88889464
|
||||
account testuser
|
||||
password Test@123456
|
||||
status 1(正常)
|
||||
gid 2(普通会员)
|
||||
|
||||
## 1. 通用约定
|
||||
|
||||
### 1.1 响应结构(统一 JSON)
|
||||
所有接口均返回以下结构(强制 JSON,不受请求类型影响):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {},
|
||||
"timestamp": 1765900000
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | int | `0`=成功;`1`=业务失败;`401`=未登录/鉴权失败 |
|
||||
| `msg` | string | 提示信息 |
|
||||
| `data` | object/null | 业务数据;登录类接口含 `access_token` 等 |
|
||||
| `timestamp` | int | 服务器时间戳(秒) |
|
||||
|
||||
### 1.2 业务 code 与 HTTP 状态码
|
||||
- 业务 `code` 与 HTTP 状态码**解耦**:未登录时返回 `code=401` 且 HTTP 状态也为 `401`。
|
||||
- 业务失败时 HTTP 状态保持 `200`,仅 `code` 非 0(除 401 外)。
|
||||
|
||||
### 1.3 鉴权(Bearer Token)
|
||||
- 登录 / 注册成功后,响应 `data` 内含 `access_token`、`refresh_token`、`expires_in`。
|
||||
- 需登录接口在请求头携带:
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
- 跨域:路由已挂载 `AllowCrossDomain` 中间件,前端联调无 CORS 问题。
|
||||
|
||||
### 1.4 接口清单一览
|
||||
| 接口 | 方法 | 鉴权 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `/common/init` | GET | 否 | 启动初始化(站点/注册方式/版本) |
|
||||
| `/common/config` | GET | 否 | 站点配置 |
|
||||
| `/common/upload` | POST | 是 | 文件上传 |
|
||||
| `/user/register` | POST | 否 | 注册 |
|
||||
| `/user/login` | POST | 否 | 账号密码登录 |
|
||||
| `/user/loginBySms` | POST | 否 | 短信验证码登录(未注册自动注册) |
|
||||
| `/user/refresh` | POST | 否(refresh_token) | 刷新 access_token |
|
||||
| `/user/logout` | POST | 是 | 退出登录 |
|
||||
| `/user/profile` | GET | 是 | 获取会员资料 |
|
||||
| `/user/updateProfile` | POST | 是 | 修改资料 |
|
||||
| `/user/changePwd` | POST | 是 | 修改密码 |
|
||||
| `/user/resetPwdBySms` | POST | 否 | 短信重置密码 |
|
||||
| `/user/resetPwdByEmail` | POST | 否 | 邮箱重置密码 |
|
||||
| `/sms/send` | POST | 否 | 发送短信验证码 |
|
||||
| `/sms/check` | POST | 否 | 校验短信验证码 |
|
||||
| `/ems/send` | POST | 否 | 发送邮箱验证码 |
|
||||
| `/ems/check` | POST | 否 | 校验邮箱验证码 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口详情
|
||||
|
||||
### 2.1 Common
|
||||
|
||||
#### GET /common/init
|
||||
启动初始化,App 冷启动首屏调用。
|
||||
- 响应示例:
|
||||
```json
|
||||
{ "code": 0, "msg": "success", "data": { "site": {...}, "register_type": "all", "version": "1.0.0" }, "timestamp": 1765900000 }
|
||||
```
|
||||
|
||||
#### GET /common/config
|
||||
站点配置(公开)。
|
||||
- 响应 `data` 结构同 `init` 中的 `site` 字段。
|
||||
|
||||
#### POST /common/upload
|
||||
文件上传(需登录),`Content-Type: multipart/form-data`,字段名 `file`。
|
||||
- 成功响应:
|
||||
```json
|
||||
{ "code": 0, "msg": "上传成功", "data": { "url": "/storage/uploads/xxx.png", "path": "uploads/xxx.png" }, "timestamp": 1765900000 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 User
|
||||
|
||||
#### POST /user/register
|
||||
注册账号。
|
||||
- 请求体(x-www-form-urlencoded):`account`(必填), `password`(必填,≥6位), `nickname`(可选), `mobile`(可选), `email`(可选), `captcha`(可选, 手机号注册时必填且需先调 `/sms/send`)
|
||||
- 成功响应(自动签发 token):
|
||||
```json
|
||||
{
|
||||
"code": 0, "msg": "注册成功", "timestamp": 1765900000,
|
||||
"data": { "uid": 1, "account": "testuser", "nickname": "测试", "access_token": "...", "refresh_token": "...", "expires_in": 3600, "token_type": "Bearer" },
|
||||
"access_token": "...", "access_expired": 3600, "refresh_token": "...", "refresh_expired": 86400
|
||||
}
|
||||
```
|
||||
> 说明:token 同时出现在 `data` 内与响应顶层(供不同客户端习惯取用)。
|
||||
|
||||
#### POST /user/login
|
||||
账号密码登录。
|
||||
- 请求体:`account`(必填), `password`(必填)
|
||||
- 响应结构同 `register`。
|
||||
|
||||
#### POST /user/loginBySms
|
||||
短信验证码登录;手机号未注册则自动注册。
|
||||
- 请求体:`mobile`(必填), `captcha`(必填)
|
||||
- 响应结构同 `register`,自动注册时初始密码为随机值(仅短信登录可用)。
|
||||
|
||||
#### POST /user/refresh
|
||||
使用 refresh_token 换取新 access_token。
|
||||
- 请求体:`refresh_token`(必填)
|
||||
- 成功响应:
|
||||
```json
|
||||
{ "code": 0, "msg": "刷新成功", "data": { "access_token": "...", "refresh_token": "...", "expires_in": 3600, "token_type": "Bearer" }, "timestamp": 1765900000 }
|
||||
```
|
||||
- 失败(refresh_token 无效/过期):`code=401`,HTTP 401。
|
||||
|
||||
#### POST /user/logout
|
||||
退出登录(需登录)。
|
||||
- 响应:`{ "code": 0, "msg": "已登出", "data": [], "timestamp": 1765900000 }`
|
||||
|
||||
#### GET /user/profile
|
||||
获取当前会员资料(需登录,Header 带 `Authorization: Bearer <token>`)。
|
||||
- 响应 `data` 字段(password/delete_at 已隐藏,附加 status_text/last_login):
|
||||
```json
|
||||
{
|
||||
"code": 0, "msg": "success",
|
||||
"data": { "uid": 1, "account": "testuser", "nickname": "测试", "mobile": "", "email": "", "avatar": "", "status": 1, "status_text": "正常", "last_login": "...", "create_at": 1765900000 },
|
||||
"timestamp": 1765900000
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /user/updateProfile
|
||||
修改资料(需登录)。
|
||||
- 请求体(均可选):`nickname`, `mobile`, `email`, `avatar`
|
||||
- 响应 `data` 为更新后的会员数组。
|
||||
|
||||
#### POST /user/changePwd
|
||||
修改密码(需登录)。
|
||||
- 请求体:`oldpassword`(必填), `newpassword`(必填,≥6位)
|
||||
- 错误示例:`{ "code": 1, "msg": "原密码错误", "timestamp": 1765900000 }`
|
||||
|
||||
#### POST /user/resetPwdBySms
|
||||
短信重置密码(免登录)。
|
||||
- 请求体:`mobile`(必填), `captcha`(必填), `newpassword`(必填,≥6位)
|
||||
|
||||
#### POST /user/resetPwdByEmail
|
||||
邮箱重置密码(免登录)。
|
||||
- 请求体:`email`(必填), `captcha`(必填), `newpassword`(必填,≥6位)
|
||||
|
||||
---
|
||||
|
||||
### 2.3 验证码
|
||||
|
||||
#### POST /sms/send
|
||||
发送短信验证码。
|
||||
- 请求体:`mobile`(必填), `event`(必填, 如 register/login/mobilelogin/resetpwd/changepwd/changemobile)
|
||||
- 频率限制:同手机号 60 秒内不可重复;同 IP 1 小时上限 5 条。
|
||||
|
||||
#### POST /sms/check
|
||||
校验短信验证码。
|
||||
- 请求体:`mobile`(必填), `event`(必填), `captcha`(必填)
|
||||
|
||||
#### POST /ems/send
|
||||
发送邮箱验证码。请求体:`email`(必填), `event`(可选, 默认 register)。
|
||||
|
||||
#### POST /ems/check
|
||||
校验邮箱验证码。请求体:`email`(必填), `event`(可选), `captcha`(必填)。
|
||||
|
||||
---
|
||||
|
||||
## 3. Apifox / Postman 联调步骤
|
||||
|
||||
1. 导入集合文件 `docs/postman/ywxapp-api-v1.postman_collection.json`(Postman 直接导入;Apifox 选择「导入 → Postman 格式」)。
|
||||
2. 设置环境/变量 `base_url` 为你的本地域名,例如 `http://localhost/api/v1`。
|
||||
3. 先调试公开接口 `GET /common/init`,确认返回 JSON(非 HTML)即链路通。
|
||||
4. 调 `POST /user/login`(或先 `register`),登录成功后在 Tests 脚本里自动把 `access_token` 写入变量 `token`。
|
||||
5. 之后调 `/user/profile` 等需登录接口,自动带 `Authorization: Bearer {{token}}`。
|
||||
6. 故意不带 token 调 `/user/profile`,应得 `code=401` + HTTP 401。
|
||||
|
||||
### 排错
|
||||
- **全部 404**:清除路由缓存 `runtime/route_list.php` 后重试(TP 缓存机制)。
|
||||
- **返回 HTML 而非 JSON**:确认路径为小写 `/api/v1/...`,且域名正确(非后台 admin 域)。
|
||||
- **401 但 token 有效**:检查 token 是否过期,`/user/refresh` 用 `refresh_token` 换新;注意 access_token 默认有效期见 `expires_in`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 测试 Checklist(建议用例)
|
||||
- [ ] init / config 公开可访问且返回 JSON
|
||||
- [ ] register 正常注册并拿到 token
|
||||
- [ ] register 重复账号 → `code=1` 提示「该账号已被注册」
|
||||
- [ ] login 密码错误 → `code=1` 提示「密码错误」
|
||||
- [ ] login 成功后 profile 返回会员资料
|
||||
- [ ] profile 不带 token → `code=401` HTTP 401
|
||||
- [ ] refresh 用 refresh_token 换发新 token 成功
|
||||
- [ ] refresh 用错误 token → `code=401`
|
||||
- [ ] changePwd 原密码错误 → `code=1`
|
||||
- [ ] logout 成功 → `code=0`
|
||||
@@ -0,0 +1,61 @@
|
||||
-- ============================================================
|
||||
-- 积分规则表 score_rule:name → title 字段重命名升级脚本
|
||||
-- 作用:按「禁用裸 name 业务字段」命名规范(name 是浏览器/layui/TP
|
||||
-- 保留语义),把积分规则表的 name 列(规则名称)改名为 title,
|
||||
-- 与 install.sql / create_operate_tables.sql 的新定义保持一致。
|
||||
-- 适用范围:已部署老库(新装库直接使用新定义,无需执行本脚本)。
|
||||
-- 特性:
|
||||
-- 1) 自动探测表前缀——无需手工替换,匹配任意 `<前缀>_score_rule` 表
|
||||
-- (如 wxapp_、ywx_、yourown_)。
|
||||
-- 2) 幂等:仅当「name 列存在 && title 列不存在」时才执行 CHANGE;
|
||||
-- 重复执行安全。
|
||||
-- 执行:
|
||||
-- mysql -u<user> -p <database> < rename_score_rule_name_to_title.sql
|
||||
-- 或 phpMyAdmin / Navicat 直接导入。
|
||||
-- ============================================================
|
||||
|
||||
DELIMITER //
|
||||
|
||||
DROP PROCEDURE IF EXISTS `__migrate_score_rule_name`//
|
||||
|
||||
CREATE PROCEDURE `__migrate_score_rule_name`()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT 0;
|
||||
DECLARE tbl VARCHAR(64);
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT c1.TABLE_NAME
|
||||
FROM information_schema.COLUMNS c1
|
||||
WHERE c1.TABLE_SCHEMA = DATABASE()
|
||||
AND c1.TABLE_NAME LIKE '%\_score\_rule'
|
||||
AND c1.COLUMN_NAME = 'name'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.COLUMNS c2
|
||||
WHERE c2.TABLE_SCHEMA = c1.TABLE_SCHEMA
|
||||
AND c2.TABLE_NAME = c1.TABLE_NAME
|
||||
AND c2.COLUMN_NAME = 'title'
|
||||
);
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
|
||||
|
||||
OPEN cur;
|
||||
read_loop: LOOP
|
||||
FETCH cur INTO tbl;
|
||||
IF done = 1 THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
-- 与 install.sql 中 score_rule.name 的列定义保持一致,仅换列名
|
||||
SET @sql = CONCAT(
|
||||
'ALTER TABLE `', tbl, '` CHANGE COLUMN `name` `title` ',
|
||||
'varchar(100) NOT NULL DEFAULT '''' COMMENT ''规则名称'''
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END LOOP;
|
||||
CLOSE cur;
|
||||
END//
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
CALL __migrate_score_rule_name();
|
||||
|
||||
DROP PROCEDURE IF EXISTS `__migrate_score_rule_name`;
|
||||
@@ -0,0 +1,147 @@
|
||||
-- ============================================================
|
||||
-- 表名规范化迁移 v2:通用表加 common_ / 会员相关表加 member_ 前缀
|
||||
-- 作用:将老库中无前缀的业务表重命名为带前缀的新表名,
|
||||
-- 与 public/install/install.sql 最新定义保持一致。
|
||||
-- 覆盖映射(前缀自动探测,任意数据库前缀通用):
|
||||
-- addon -> common_addon
|
||||
-- attachment -> common_attachment
|
||||
-- configure -> common_configure
|
||||
-- links -> common_links
|
||||
-- spider_log -> common_spider_log
|
||||
-- spider_stat -> common_spider_stat
|
||||
-- sms -> common_sms
|
||||
-- member_wallets-> member_wallet (去复数,不加前缀)
|
||||
-- notice -> common_notice
|
||||
-- ad -> common_ad
|
||||
-- task -> common_task
|
||||
-- prop -> common_prop
|
||||
-- medal -> common_medal
|
||||
-- help -> common_help
|
||||
-- card -> common_card
|
||||
-- score_rule -> member_score_rule
|
||||
-- score_log -> member_score_log
|
||||
-- addon_config -> common_addonconf (框架统一插件配置表)
|
||||
-- 字段变更:
|
||||
-- common_nav_menu 新增 app varchar(30) NOT NULL DEFAULT ''(url 之后)
|
||||
-- 说明:
|
||||
-- 1) 自动从框架核心表 backend_admin 反推表前缀(如 wxapp_),
|
||||
-- 无需手工替换前缀。
|
||||
-- 2) 幂等:仅当「旧表存在 && 新表不存在」才 RENAME,重复执行安全。
|
||||
-- 3) RENAME TABLE 是原子操作,不影响列定义与数据。
|
||||
-- 4) 若库中同时存在旧表与新表(数据分裂),旧表将被跳过、不合并,
|
||||
-- 请先人工核对处理再执行。
|
||||
-- 5) 列级变更(nav_menu.app)通过 information_schema 检查,已存在则跳过。
|
||||
-- ============================================================
|
||||
|
||||
DELIMITER //
|
||||
|
||||
-- 单表重命名:旧表存在且新表不存在时才执行
|
||||
DROP PROCEDURE IF EXISTS `__rename_if`//
|
||||
CREATE PROCEDURE `__rename_if`(IN p_old VARCHAR(64), IN p_new VARCHAR(64))
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = p_old
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = p_new
|
||||
) THEN
|
||||
SET @sql = CONCAT('RENAME TABLE `', p_old, '` TO `', p_new, '`');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END//
|
||||
|
||||
-- 加列:列不存在时才加(幂等)
|
||||
DROP PROCEDURE IF EXISTS `__add_col_if_missing`//
|
||||
CREATE PROCEDURE `__add_col_if_missing`(IN p_table VARCHAR(64), IN p_column VARCHAR(64), IN p_definition TEXT)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = p_table
|
||||
AND COLUMN_NAME = p_column
|
||||
) AND EXISTS (
|
||||
SELECT 1 FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = p_table
|
||||
) THEN
|
||||
SET @sql = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN `', p_column, '` ', p_definition);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END//
|
||||
|
||||
-- 主流程:探测前缀 + 批量重命名 + 加列
|
||||
DROP PROCEDURE IF EXISTS `__migrate_table_prefix`//
|
||||
CREATE PROCEDURE `__migrate_table_prefix`()
|
||||
BEGIN
|
||||
DECLARE v_prefix VARCHAR(64) DEFAULT '';
|
||||
DECLARE v_p VARCHAR(70) DEFAULT '';
|
||||
|
||||
-- 1) 反推表前缀:wxapp_backend_admin -> wxapp(不含尾部下划线)
|
||||
SELECT SUBSTRING_INDEX(TABLE_NAME, '_backend_admin', 1) INTO v_prefix
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME LIKE '%\_backend\_admin'
|
||||
LIMIT 1;
|
||||
|
||||
-- 2) 兜底:无 backend_admin 的库尝试用导航表反推
|
||||
IF v_prefix = '' THEN
|
||||
SELECT SUBSTRING_INDEX(TABLE_NAME, '_common_nav_menu', 1) INTO v_prefix
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME LIKE '%\_common\_nav\_menu'
|
||||
LIMIT 1;
|
||||
END IF;
|
||||
|
||||
-- 3) 无法探测则输出提示并终止
|
||||
IF v_prefix = '' THEN
|
||||
SELECT '无法自动探测表前缀,请将脚本中 CONCAT(v_prefix,...) 替换为真实前缀(如 wxapp_)后执行' AS notice;
|
||||
ELSE
|
||||
SET v_p = CONCAT(v_prefix, '_');
|
||||
|
||||
-- 表重命名(17 张)
|
||||
CALL __rename_if(CONCAT(v_p, 'addon'), CONCAT(v_p, 'common_addon'));
|
||||
CALL __rename_if(CONCAT(v_p, 'attachment'), CONCAT(v_p, 'common_attachment'));
|
||||
CALL __rename_if(CONCAT(v_p, 'configure'), CONCAT(v_p, 'common_configure'));
|
||||
CALL __rename_if(CONCAT(v_p, 'links'), CONCAT(v_p, 'common_links'));
|
||||
CALL __rename_if(CONCAT(v_p, 'spider_log'), CONCAT(v_p, 'common_spider_log'));
|
||||
CALL __rename_if(CONCAT(v_p, 'spider_stat'), CONCAT(v_p, 'common_spider_stat'));
|
||||
CALL __rename_if(CONCAT(v_p, 'sms'), CONCAT(v_p, 'common_sms'));
|
||||
CALL __rename_if(CONCAT(v_p, 'member_wallets'),CONCAT(v_p, 'member_wallet'));
|
||||
CALL __rename_if(CONCAT(v_p, 'notice'), CONCAT(v_p, 'common_notice'));
|
||||
CALL __rename_if(CONCAT(v_p, 'ad'), CONCAT(v_p, 'common_ad'));
|
||||
CALL __rename_if(CONCAT(v_p, 'task'), CONCAT(v_p, 'common_task'));
|
||||
CALL __rename_if(CONCAT(v_p, 'prop'), CONCAT(v_p, 'common_prop'));
|
||||
CALL __rename_if(CONCAT(v_p, 'medal'), CONCAT(v_p, 'common_medal'));
|
||||
CALL __rename_if(CONCAT(v_p, 'help'), CONCAT(v_p, 'common_help'));
|
||||
CALL __rename_if(CONCAT(v_p, 'card'), CONCAT(v_p, 'common_card'));
|
||||
CALL __rename_if(CONCAT(v_p, 'score_rule'), CONCAT(v_p, 'member_score_rule'));
|
||||
CALL __rename_if(CONCAT(v_p, 'score_log'), CONCAT(v_p, 'member_score_log'));
|
||||
CALL __rename_if(CONCAT(v_p, 'addon_config'), CONCAT(v_p, 'common_addonconf'));
|
||||
|
||||
-- 字段变更:导航菜单新增 app 列(url 之后)
|
||||
CALL __add_col_if_missing(
|
||||
CONCAT(v_p, 'common_nav_menu'),
|
||||
'app',
|
||||
"varchar(30) NOT NULL DEFAULT '' COMMENT '所属应用(跨应用链接用,空=当前应用)' AFTER `url`"
|
||||
);
|
||||
|
||||
-- 数据修复:软删除字段值为 NULL/空 的行(defaultSoftDelete=0 的表会被误过滤,导致角色/菜单查不到)
|
||||
-- 幂等:已修复为 0 的行不受影响,可重复执行
|
||||
SET @sql := CONCAT('UPDATE `', v_p, 'backend_role` SET `delete_at` = 0 WHERE `delete_at` IS NULL OR `delete_at` = ''''');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END//
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
CALL __migrate_table_prefix();
|
||||
|
||||
DROP PROCEDURE IF EXISTS `__rename_if`;
|
||||
DROP PROCEDURE IF EXISTS `__add_col_if_missing`;
|
||||
DROP PROCEDURE IF EXISTS `__migrate_table_prefix`;
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "YwxApp API v1",
|
||||
"description": "YwxApp 标准 App 接口集合(/api/v1)。code=0 成功,非0 失败,401 未登录。登录后从响应取 access_token,通过 Tests 脚本自动写入环境变量 token。",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{ "key": "base_url", "value": "http://localhost/api/v1", "type": "string" },
|
||||
{ "key": "token", "value": "", "type": "string" }
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "Common",
|
||||
"item": [
|
||||
{
|
||||
"name": "init 启动初始化",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{base_url}}/common/init"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "config 站点配置",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{base_url}}/common/config"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "User",
|
||||
"item": [
|
||||
{
|
||||
"name": "register 注册",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{base_url}}/user/register",
|
||||
"body": {
|
||||
"mode": "urlencoded",
|
||||
"urlencoded": [
|
||||
{ "key": "account", "value": "testuser", "type": "text" },
|
||||
{ "key": "password", "value": "123456", "type": "text" },
|
||||
{ "key": "nickname", "value": "测试用户", "type": "text" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "login 登录",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{base_url}}/user/login",
|
||||
"body": {
|
||||
"mode": "urlencoded",
|
||||
"urlencoded": [
|
||||
{ "key": "account", "value": "testuser", "type": "text" },
|
||||
{ "key": "password", "value": "123456", "type": "text" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"var json = pm.response.json();",
|
||||
"if (json.code === 0 && json.data && json.data.access_token) {",
|
||||
" pm.collectionVariables.set('token', json.data.access_token);",
|
||||
" pm.collectionVariables.set('refresh_token', json.data.refresh_token);",
|
||||
"}"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "refresh 刷新token",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{base_url}}/user/refresh",
|
||||
"body": {
|
||||
"mode": "urlencoded",
|
||||
"urlencoded": [
|
||||
{ "key": "refresh_token", "value": "{{refresh_token}}", "type": "text" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"var json = pm.response.json();",
|
||||
"if (json.code === 0 && json.data && json.data.access_token) {",
|
||||
" pm.collectionVariables.set('token', json.data.access_token);",
|
||||
"}"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "profile 获取资料(需登录)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{base_url}}/user/profile",
|
||||
"header": [
|
||||
{ "key": "Authorization", "value": "Bearer {{token}}" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "updateProfile 修改资料(需登录)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{base_url}}/user/updateProfile",
|
||||
"header": [
|
||||
{ "key": "Authorization", "value": "Bearer {{token}}" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "urlencoded",
|
||||
"urlencoded": [
|
||||
{ "key": "nickname", "value": "新昵称", "type": "text" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "changePwd 修改密码(需登录)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{base_url}}/user/changePwd",
|
||||
"header": [
|
||||
{ "key": "Authorization", "value": "Bearer {{token}}" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "urlencoded",
|
||||
"urlencoded": [
|
||||
{ "key": "oldpassword", "value": "123456", "type": "text" },
|
||||
{ "key": "newpassword", "value": "654321", "type": "text" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logout 退出登录(需登录)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{base_url}}/user/logout",
|
||||
"header": [
|
||||
{ "key": "Authorization", "value": "Bearer {{token}}" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS `__PREFIX__common_nav_menu` (
|
||||
`parent_id` int unsigned NOT NULL DEFAULT '0' COMMENT '父级ID(0=一级)',
|
||||
`title` varchar(50) NOT NULL DEFAULT '' COMMENT '菜单名称',
|
||||
`url` varchar(255) NOT NULL DEFAULT '' COMMENT '链接(controller/action 或 外链 http)',
|
||||
`app` varchar(30) NOT NULL DEFAULT '' COMMENT '所属应用(跨应用链接用,空=当前应用)',
|
||||
`icon` varchar(50) NOT NULL DEFAULT '' COMMENT '图标类',
|
||||
`sort` int NOT NULL DEFAULT '0' COMMENT '排序(越小越靠前)',
|
||||
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(1=显示 0=隐藏)',
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header"></div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表格顶部工具条 -->
|
||||
<script type="text/html" id="tableBarTpl">
|
||||
<div class="layui-btn-group">
|
||||
<a
|
||||
class="layui-btn layui-btn-sm layui-btn-primary"
|
||||
title="新建数据"
|
||||
lay-event="dataCreate"
|
||||
data-perm="role:add">
|
||||
<i class="layui-icon layui-icon-add-1"></i>
|
||||
</a>
|
||||
<a
|
||||
class="layui-btn layui-btn-sm layui-btn-primary"
|
||||
title="删除资源"
|
||||
lay-event="dataDelete"
|
||||
data-perm="role:delete"
|
||||
><i class="layui-icon layui-icon-delete"></i>
|
||||
</a>
|
||||
<a
|
||||
class="layui-btn layui-btn-sm layui-btn-primary"
|
||||
title="资源回收站"
|
||||
lay-event="dataRecybin"
|
||||
data-perm="role:recyclebin"
|
||||
><i class="layui-icon layui-icon-home"></i>
|
||||
</a>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- 表格数据工具条 -->
|
||||
<script type="text/html" id="dataBarTpl">
|
||||
<div class="layui-btn-group">
|
||||
<a
|
||||
class="layui-btn layui-btn-sm layui-btn-primary"
|
||||
title="编辑数据"
|
||||
lay-event="update"
|
||||
data-perm="role:edit"
|
||||
><i class="layui-icon layui-icon-edit"></i>
|
||||
</a>
|
||||
<a
|
||||
class="layui-btn layui-btn-sm layui-btn-primary"
|
||||
title="权限分配"
|
||||
lay-event="permission"
|
||||
data-perm="role:*"
|
||||
><i class="layui-icon layui-icon-auz"></i>
|
||||
</a>
|
||||
{{# if(d.id > 1) { }}
|
||||
<a
|
||||
class="layui-btn layui-btn-sm layui-btn-primary"
|
||||
title="删除数据"
|
||||
lay-event="delete"
|
||||
data-perm="role:delete"
|
||||
><i class="layui-icon layui-icon-delete"></i>
|
||||
</a>
|
||||
{{# } }}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- 表格回收站 -->
|
||||
<script type="text/html" id="dataRecybinTpl">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header"></div>
|
||||
<div class="layui-card-body">
|
||||
<table
|
||||
class="layui-hide"
|
||||
id="dataRecybinTable"
|
||||
lay-filter="dataRecybinTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- 角色状态 -->
|
||||
<script type="text/html" id="statusTpl">
|
||||
{{# if(d.status == 1) { }}
|
||||
<span class="layui-badge layui-bg-green">正常</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge layui-bg-orange">禁用</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<!-- 添加/编辑角色表单 -->
|
||||
<script type="text/html" id="dataFormTpl">
|
||||
<form class="layui-form layui-form-pane" lay-filter="wxapp-form" id="wxapp-form">
|
||||
<input type="hidden" name="id" value="{{d.id||''}}">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">角色名称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="title" required lay-verify="required" placeholder="请输入角色名称" autocomplete="off" class="layui-input" value="{{d.title||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">编码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="name" required lay-verify="required" placeholder="请输入分组编码" autocomplete="off" class="layui-input" value="{{d.name||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">角色描述</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="description" placeholder="请输入角色描述" class="layui-textarea">{{d.description||''}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="status" value="1" title="启用" {{ d.status==undefined || d.status==1 ? 'checked' : '' }}>
|
||||
<input type="radio" name="status" value="0" title="禁用" {{ d.status==0 ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-hide">
|
||||
<button class="layui-btn" lay-submit="" lay-filter="wxapp-form-submit" id="wxapp-form-submit">提交</button>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<!-- 权限分配弹窗 -->
|
||||
<script type="text/html" id="permissionFormTpl">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
角色:{{d.name}}
|
||||
<span style="color:#999;font-weight:normal;">(ID: {{d.id}})</span>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<div id="permissionTree"></div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script>
|
||||
|
||||
layui.use(['http', 'auth'], function () {
|
||||
var $ = layui.$, table = layui.table, form = layui.form, layer = layui.layer
|
||||
, laydate = layui.laydate, laytpl = layui.laytpl, tree = layui.tree, http = layui.http
|
||||
, auth = layui.auth;
|
||||
var _savedPerms = (layui.data('backend').permission) || [];
|
||||
auth.init(_savedPerms);
|
||||
auth.setController('group');
|
||||
var dataTable = table.render({
|
||||
elem: '#dataTable'
|
||||
, url: 'index' // 后端数据接口
|
||||
, page: true
|
||||
, limit: 15
|
||||
, toolbar: '#tableBarTpl'
|
||||
, cols: [[
|
||||
{ type: 'checkbox', fixed: 'left' }
|
||||
, { field: 'id', title: 'ID', width: 60, align: 'center', sort: true, fixed: 'left' }
|
||||
, { field: 'title', title: '名称', width: 150, align: 'center', fixed: 'left' }
|
||||
, { field: 'name', title: '代码', width: 150, align: 'center', fixed: 'left' }
|
||||
, { field: 'description', title: '描述', minWidth: 200, align: 'left' }
|
||||
, { field: 'status', title: '状态', width: 120, align: 'center', templet: '#statusTpl' }
|
||||
, { field: 'create_time', title: '创建时间', width: 160, align: 'center', sort: true }
|
||||
, { title: '操作', width: 160, align: 'left', toolbar: '#dataBarTpl', fixed: 'right', unresize: true }
|
||||
]]
|
||||
, done: function (res, curr, count) {
|
||||
var view = $('#dataTable').next('.layui-table-view');
|
||||
view.find('[data-perm]').each(function () {
|
||||
var perm = $(this).attr('data-perm');
|
||||
if (perm && !auth.has(perm)) { $(this).remove(); }
|
||||
});
|
||||
$('.layui-btn[data-perm]').each(function () {
|
||||
var perm = $(this).attr('data-perm');
|
||||
if (perm && !auth.has(perm)) { $(this).remove(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
table.on('toolbar(dataTable)', function (elem) {
|
||||
switch (elem.event) {
|
||||
case 'dataCreate':
|
||||
active.dataCreate({ pid: 0, type: 1 });
|
||||
break;
|
||||
case 'dataDelete':
|
||||
var checkStatus = table.checkStatus('dataTable'), checkData = checkStatus.data;
|
||||
let ids = checkData.map((item, index, array) => { return item.id; });
|
||||
active.dataDelete(ids);
|
||||
break;
|
||||
case 'dataRecybin':
|
||||
active.dataRecybin();
|
||||
break;
|
||||
};
|
||||
});
|
||||
|
||||
table.on('tool(dataTable)', function (elem) {
|
||||
var data = elem.data;
|
||||
switch (elem.event) {
|
||||
case 'update':
|
||||
active.dataEdit(data);
|
||||
break;
|
||||
case 'permission':
|
||||
openPermissionForm(data);
|
||||
break;
|
||||
case 'delete':
|
||||
active.dataDelete([data.id]);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
var dataFromFun = function (data, callback, done) {
|
||||
var title = data ? '编辑数据[' + data.account + ']' : '添加数据';
|
||||
var formHtml = laytpl($('#dataFormTpl').html()).render(data || {});
|
||||
layer.open({
|
||||
title: data.id ? '编辑菜单' : (data.pid ? '添加子菜单' : '添加根菜单'),
|
||||
content: formHtml,
|
||||
anim: "slideLeft",
|
||||
offset: "r",
|
||||
btnAlign: "l",
|
||||
area: ['50% ', '99%'],
|
||||
shade: 0.1,
|
||||
shadeClose: true,
|
||||
btn: ['确定', '取消'],
|
||||
success: function (layero, index) {
|
||||
callback(layero, index);
|
||||
form.render();
|
||||
},
|
||||
yes: function (index, layero) {
|
||||
window.layui.form.on('submit(wxapp-form-submit)', function (elem) {
|
||||
done(layero, index, elem);
|
||||
layui.off('submit(wxapp-form)', 'from');
|
||||
return false;
|
||||
});
|
||||
layero.contents().find("#wxapp-form-submit").trigger('click');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//事件
|
||||
var active = {
|
||||
dataDelete: function (ids, force = 0) {
|
||||
if (ids.length === 0) {
|
||||
return layer.msg('请选择数据');
|
||||
}
|
||||
layer.prompt({
|
||||
formType: 1
|
||||
, title: '敏感操作,请验证口令'
|
||||
}, function (value, index) {
|
||||
layer.close(index);
|
||||
layer.confirm('确定删除吗?', function (index) {
|
||||
http.delete("delete", { ids: ids.join(','), force: force })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
layer.close(index);
|
||||
table.reload('dataTable', {}, true);
|
||||
} else {
|
||||
layer.msg(res.msg || '操作失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
dataCreate: function (data = {}) {
|
||||
dataFromFun(data,
|
||||
function (layero, index) { },
|
||||
function (layero, index, elem) {
|
||||
var field = elem.field;
|
||||
http.post('save', field)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
layer.close(index);
|
||||
table.reload('dataTable', {}, true);
|
||||
} else {
|
||||
layer.msg(res.msg || '操作失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
dataEdit: function (data = {}) {
|
||||
dataFromFun(data,
|
||||
function (layero, index) { },
|
||||
function (layero, index, elem) {
|
||||
var field = elem.field;
|
||||
http.put('update', field)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
layer.close(index);
|
||||
table.reload('dataTable', {}, true);
|
||||
} else {
|
||||
layer.msg(res.msg || '操作失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
dataRecybin: function (data = {}) {
|
||||
var formHtml = laytpl($('#dataRecybinTpl').html()).render(data || {});
|
||||
layer.open({
|
||||
title: '资源回收站',
|
||||
content: formHtml,
|
||||
anim: "slideLeft",
|
||||
offset: "r",
|
||||
area: ['50%', '99%'],
|
||||
shade: 0.1,
|
||||
shadeClose: true,
|
||||
success: function (layero, index) {
|
||||
table.render({
|
||||
elem: '#dataRecybinTable',
|
||||
url: "recyclebin",
|
||||
defaultToolbar: [{
|
||||
title: '批量删除数据',
|
||||
layEvent: 'dataDelete',
|
||||
icon: 'layui-icon-delete',
|
||||
onClick: function (obj) {
|
||||
var checkStatus = table.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
|
||||
let ids = checkData.map((item, index, array) => { return item.id; });
|
||||
console.log(ids);
|
||||
}
|
||||
}, 'filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{ type: 'checkbox', fixed: 'left' },
|
||||
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
|
||||
{ field: 'title', title: '名称', width: 180, fixed: 'left' },
|
||||
{ field: 'name', title: '代码', width: 180, fixed: 'left' },
|
||||
{ field: 'description', title: '描述', minWidth: 200, sort: true },
|
||||
{
|
||||
fixed: "right", title: "操作", width: 120, align: "center", templet: function (d) {
|
||||
return `<div class="layui-btn-group">
|
||||
<a class="layui-btn layui-btn-sm" title="恢复数据" lay-event="restore" > <i class="layui-icon layui-icon-edit"></i> </a >
|
||||
<a class="layui-btn layui-btn-sm" title="删除数据" lay-event="delete"><i class="layui-icon layui-icon-delete"></i> </a>
|
||||
</div >`;
|
||||
}
|
||||
}
|
||||
]],
|
||||
page: true,
|
||||
done: function () {
|
||||
table.on('tool(dataRecybinTable)', function (elem) {
|
||||
var data = elem.data;
|
||||
switch (elem.event) {
|
||||
case 'restore':
|
||||
active.dataRestore([data.id]);
|
||||
break;
|
||||
case 'delete':
|
||||
active.dataDelete([data.id], 1);
|
||||
break;
|
||||
}
|
||||
});
|
||||
table.on('toolbar(dataRecybinTable)', function (elem) {
|
||||
switch (elem.event) {
|
||||
case 'dataDelete':
|
||||
var checkStatus = table.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
|
||||
let idss = checkData.map((item, index, array) => { return item.id; });
|
||||
active.dataDelete(idss, 1);
|
||||
break;
|
||||
case 'dataRestore':
|
||||
var checkStatus = table.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
|
||||
let ids = checkData.map((item, index, array) => { return item.id; });
|
||||
active.dataRestore(ids);
|
||||
break;
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
dataRestore: function (ids) {
|
||||
if (ids.length === 0) {
|
||||
return layer.msg('请选择数据');
|
||||
}
|
||||
http.put("restore", { ids: ids.join(',') })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
layer.msg('恢复成功');
|
||||
table.reload('dataTable', {}, true);
|
||||
} else {
|
||||
layer.msg(res.msg || '恢复失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 打开权限分配表单
|
||||
function openPermissionForm(data) {
|
||||
var title = '分配权限 [' + data.name + ']';
|
||||
var formHtml = laytpl($('#permissionFormTpl').html()).render(data);
|
||||
var index = layer.open({
|
||||
anim: "slideLeft",
|
||||
offset: "r",
|
||||
btnAlign: "l",
|
||||
area: ['50% ', '99%'],
|
||||
shade: 0.1,
|
||||
shadeClose: true,
|
||||
content: formHtml,
|
||||
success: function (layero, index) {
|
||||
http.get("permission", { id: data.id })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
tree.render({
|
||||
elem: '#permissionTree',
|
||||
data: res.data,
|
||||
showCheckbox: true,
|
||||
id: 'permissionTree'
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.msg || '获取权限失败', { icon: 2 });
|
||||
}
|
||||
});
|
||||
},
|
||||
btn: ['保存', '取消'],
|
||||
yes: function (index, layero) {
|
||||
var checkedData = layui.tree.getChecked('permissionTree');
|
||||
var permissionIds = [];
|
||||
function getIds(data) {
|
||||
data.forEach(function (item) {
|
||||
permissionIds.push(item.id);
|
||||
if (item.children && item.children.length > 0) {
|
||||
getIds(item.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
getIds(checkedData);
|
||||
http.put("permission", {
|
||||
id: data.id,
|
||||
permissions: permissionIds
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
layer.msg(res.msg || '保存成功', { icon: 1, time: 1000 }, function () {
|
||||
layer.close(index);
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.msg || '保存失败', { icon: 2 });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,435 @@
|
||||
<!--
|
||||
* @Author: YwxApp <ywx@ywxapp.cn>
|
||||
* @Date: 2026-04-29 02:32:33
|
||||
* @LastEditors: YwxApp <ywx@ywxapp.cn>
|
||||
* @LastEditTime: 2026-08-18 14:08:16
|
||||
* @Description:
|
||||
* @FilePath: \ywxapp_dev\docs\tpl\treetable.html
|
||||
* @CustomString: Copyright (c) 2026 YwxApp
|
||||
-->
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header"> </div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/html" id="tableBarTpl">
|
||||
<div class="layui-btn-group">
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="新建节点" lay-event="dataCreate" data-perm="power:add"> <i class="layui-icon layui-icon-add-1"></i> </a>
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="删除资源" lay-event="dataDelete" data-perm="power:delete"><i class="layui-icon layui-icon-delete"></i> </a>
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="资源回收站" lay-event="dataRecybin" data-perm="power:recyclebin"><i class="layui-icon layui-icon-home"></i> </a>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="dataBarTpl">
|
||||
<div class="layui-btn-group">
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="编辑节点" lay-event="update" data-perm="power:edit"><i class="layui-icon layui-icon-edit"></i> </a>
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="添加子节点" lay-event="create" data-perm="power:add"><i class="layui-icon layui-icon-add-1"></i> </a>
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="删除节点" lay-event="delete" data-perm="power:delete"><i class="layui-icon layui-icon-delete"></i> </a>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 添加/编辑菜单表单模板 -->
|
||||
<script type="text/html" id="dataFormTpl">
|
||||
<form class="layui-form layui-form-pane" lay-filter="wxapp-form" id="wxapp-form">
|
||||
<input type="hidden" name="id" value="{{= d.id || '' }}">
|
||||
<input type="hidden" name="pid" value="{{= d.pid || 0 }}">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">名称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="title" required lay-verify="required" placeholder="请输入菜单名称" value="{{ d.title || '' }}" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">标识</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="name" required lay-verify="required" placeholder="请输入菜单标识" value="{{ d.name || '' }}" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">菜单类型</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="type" lay-filter="menuType" lay-verify="required">
|
||||
<option value="1" {{ (d.type == 1 || !d.type) ? 'selected' : '' }}>目录</option>
|
||||
<option value="2" {{ d.type == 2 ? 'selected' : '' }}>菜单</option>
|
||||
<option value="3" {{ d.type == 3 ? 'selected' : '' }}>按钮</option>
|
||||
<option value="4" {{ d.type == 4 ? 'selected' : '' }}>API</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-item-url {{ d.type == 3 ? 'layui-hide' : '' }}" id="urlField">
|
||||
<label class="layui-form-label">路由</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="route" placeholder="请输入路由路径" value="{{ d.route || '' }}" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">图标</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="icon" id="iconPicker" placeholder="请输入图标类名" value="{{ d.icon || '' }}" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">排序</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="sort" value="{{ d.sort || 0 }}" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="status" lay-skin="switch" lay-text="启用|禁用" {{ d.status != 0 ? 'checked' : (!d.id ? 'checked' : '') }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-hide">
|
||||
<button class="layui-btn" lay-submit="" lay-filter="wxapp-form-submit" id="wxapp-form-submit">提交</button>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
<!-- 状态开关 -->
|
||||
<script type="text/html" id="statusTpl">
|
||||
<input type="checkbox" name="status" value="{{d.id}}" lay-skin="switch" lay-text="启用|禁用" lay-filter="statusSwitch" {{ d.status == 1 ? 'checked' : '' }}>
|
||||
</script>
|
||||
<script type="text/html" id="dataRecybinTpl">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header"> </div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-hide" id="dataRecybinTable" lay-filter="dataRecybinTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
</script>
|
||||
<script type="text/html" id="dataRecybinTpl">
|
||||
<div class="layui-btn-group">
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="恢复数据" lay-event="update"><i class="layui-icon layui-icon-edit"></i> </a>
|
||||
<a class="layui-btn layui-btn-sm layui-btn-primary" title="删除节点" lay-event="delete"><i class="layui-icon layui-icon-delete"></i> </a>
|
||||
</div>
|
||||
</script>
|
||||
<script>
|
||||
layui.use(['layer', 'http', 'iconPicker', 'auth'], function () {
|
||||
var $ = layui.$;
|
||||
var treeTable = layui.treeTable;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var laytpl = layui.laytpl;
|
||||
var iconPicker = layui.iconPicker;
|
||||
var http = layui.http;
|
||||
var auth = layui.auth;
|
||||
var _savedPerms = (layui.data('backend').permission) || [];
|
||||
auth.init(_savedPerms);
|
||||
auth.setController('power');
|
||||
treeTable.render({
|
||||
elem: '#dataTable',
|
||||
url: 'index',
|
||||
parseData: function (res) { // 预处理返回数据
|
||||
console.log('原始数据:', res); // 调试输入
|
||||
return {
|
||||
code: res.code === 0 ? 0 : 1, // 0 标识成功
|
||||
data: res.data || [], // 数据数组
|
||||
msg: res.message || '' // 错误信息
|
||||
};
|
||||
},
|
||||
tree: {
|
||||
customName: {
|
||||
children: "children",
|
||||
isParent: "is_parent",
|
||||
name: "title",
|
||||
id: "id",
|
||||
pid: "pid",
|
||||
icon: "icon"
|
||||
},
|
||||
data: { isSimpleData: true, rootPid: 0 },
|
||||
view: {},
|
||||
async: {},
|
||||
callback: {}
|
||||
},
|
||||
height: 'full-100',
|
||||
toolbar: '#tableBarTpl',
|
||||
cols: [[
|
||||
{ type: 'checkbox', fixed: 'left' },
|
||||
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
|
||||
{ field: 'title', title: 'title', width: 180, fixed: 'left' },
|
||||
{ field: 'name', title: 'name', width: 180, fixed: 'left' },
|
||||
{
|
||||
field: 'type', title: '类型', width: 80, templet: function (d) {
|
||||
if (d.type === 1) {
|
||||
return '<span class="layui-badge layui-bg-gray">目录</span>';
|
||||
} else if (d.type === 2) {
|
||||
return '<span class="layui-badge layui-bg-gray">菜单</span>';
|
||||
} else if (d.type === 3) {
|
||||
return '<span class="layui-badge layui-bg-gray">按钮</span>';
|
||||
} else if (d.type === 4) {
|
||||
return '<span class="layui-badge layui-bg-gray">API</span>';
|
||||
}
|
||||
}
|
||||
}, { field: 'route', title: '路由', minWidth: 200, sort: true },
|
||||
{
|
||||
field: 'icon', title: '图标', width: 80, sort: true, templet: function (d) {
|
||||
return '<i class="layui-icon ' + d.icon + '"></i> ';
|
||||
}
|
||||
},
|
||||
{ field: 'sort', title: '排序', width: 80, sort: true },
|
||||
{ field: 'status', title: '状态', width: 96, align: 'center', templet: '#statusTpl' },
|
||||
{ fixed: "right", title: "操作", width: 181, align: "center", toolbar: "#dataBarTpl" }
|
||||
]],
|
||||
page: true,
|
||||
done: function (res, curr, count, origin) {
|
||||
var view = $('#dataTable').next('.layui-table-view');
|
||||
view.find('[data-perm]').each(function () {
|
||||
var perm = $(this).attr('data-perm');
|
||||
if (perm && !auth.has(perm)) { $(this).remove(); }
|
||||
});
|
||||
$('.layui-btn[data-perm]').each(function () {
|
||||
var perm = $(this).attr('data-perm');
|
||||
if (perm && !auth.has(perm)) { $(this).remove(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
treeTable.on('toolbar(dataTable)', function (obj) {
|
||||
var options = obj.config;
|
||||
switch (obj.event) {
|
||||
case 'dataCreate':
|
||||
active.dataCreate({ pid: 0, type: 1 });
|
||||
break;
|
||||
case 'dataDelete':
|
||||
var checkStatus = treeTable.checkStatus('dataTable'), checkData = checkStatus.data;
|
||||
let ids = checkData.map((item, index, array) => { return item.id; });
|
||||
active.dataDelete(ids);
|
||||
break;
|
||||
case 'dataRecybin':
|
||||
active.dataRecybin();
|
||||
break;
|
||||
};
|
||||
});
|
||||
treeTable.on('tool(dataTable)', function (elem) {
|
||||
var data = elem.data;
|
||||
switch (elem.event) {
|
||||
case 'update':
|
||||
active.dataEdit(data);
|
||||
break;
|
||||
case 'create':
|
||||
active.dataCreate({ pid: data.id });
|
||||
break;
|
||||
case 'delete':
|
||||
active.dataDelete([data.id]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
// 状态开关
|
||||
form.on('switch(statusSwitch)', function (obj) {
|
||||
var id = this.value;
|
||||
var status = obj.elem.checked ? 1 : 0;
|
||||
layer.confirm('确定要' + (status ? '启用' : '禁用') + '该数据吗?', function (index) {
|
||||
layui.request.post('update', { id: id, status: status }).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg(res.msg, { icon: 1 });
|
||||
table.reload('dataTable', {}, true);
|
||||
} else {
|
||||
layer.msg(res.msg, { icon: 2 });
|
||||
obj.elem.checked = !obj.elem.checked;
|
||||
form.render('checkbox');
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
}, function () {
|
||||
obj.elem.checked = !obj.elem.checked;
|
||||
form.render('checkbox');
|
||||
});
|
||||
});
|
||||
|
||||
var dataFromFun = function (data, callback, done) {
|
||||
var formHtml = laytpl($('#dataFormTpl').html()).render(data || {});
|
||||
layer.open({
|
||||
title: data.id ? '编辑数据' : (data.pid ? '添加子数据' : '添加根数据'),
|
||||
content: formHtml,
|
||||
anim: "slideLeft",
|
||||
offset: "r",
|
||||
btnAlign: "l",
|
||||
area: ['520px', '98%'],
|
||||
shade: 0.1,
|
||||
shadeClose: true,
|
||||
btn: ['确定', '取消'],
|
||||
success: function (layero, index) {
|
||||
callback(layero, index);
|
||||
form.render();
|
||||
},
|
||||
yes: function (index, layero) {
|
||||
window.layui.form.on('submit(wxapp-form-submit)', function (elem) {
|
||||
done(layero, index, elem);
|
||||
layui.off('submit(wxapp-form)', 'from');
|
||||
return false;
|
||||
});
|
||||
layero.contents().find("#wxapp-form-submit").trigger('click');
|
||||
}
|
||||
});
|
||||
};
|
||||
//事件
|
||||
var active = {
|
||||
dataCreate: function (data = {}) {
|
||||
dataFromFun(data,
|
||||
function (layero, index) {
|
||||
iconPicker.render({
|
||||
elem: '#iconPicker',
|
||||
type: 'fontClass',
|
||||
search: true,
|
||||
page: false,
|
||||
});
|
||||
},
|
||||
function (layero, index, elem) {
|
||||
var field = elem.field;
|
||||
field.icon = 'layui-icon ' + field.icon;
|
||||
field.status = field.status ? 1 : 0;
|
||||
http.post('save', field)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
layer.close(index);
|
||||
treeTable.reload('dataTable', {}, true);
|
||||
} else {
|
||||
layer.msg(res.msg || '操作失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
dataEdit: function (data = {}) {
|
||||
dataFromFun(data,
|
||||
function (layero, index) {
|
||||
iconPicker.render({
|
||||
elem: '#iconPicker',
|
||||
type: 'fontClass',
|
||||
search: true,
|
||||
page: false,
|
||||
});
|
||||
},
|
||||
function (layero, index, elem) {
|
||||
var field = elem.field;
|
||||
field.icon = 'layui-icon ' + field.icon;
|
||||
field.status = field.status ? 1 : 0;
|
||||
http.put('update', field)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
layer.close(index);
|
||||
treeTable.reload('dataTable', {}, true);
|
||||
} else {
|
||||
layer.msg(res.msg || '操作失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
dataDelete: function (ids, force = 0) {
|
||||
if (ids.length === 0) {
|
||||
return layer.msg('请选择数据');
|
||||
}
|
||||
layer.prompt({
|
||||
formType: 1
|
||||
, title: '敏感操作,请验证口令'
|
||||
}, function (value, index) {
|
||||
layer.close(index);
|
||||
layer.confirm('确定删除吗?', function (index) {
|
||||
http.delete('delete', { ids: ids.join(','), force: force })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
layer.close(index);
|
||||
treeTable.reload('dataTable', {}, true);
|
||||
} else {
|
||||
layer.msg(res.msg || '操作失败');
|
||||
}
|
||||
});
|
||||
layer.msg('已删除');
|
||||
treeTable.reload('dataTable', {}, true);
|
||||
});
|
||||
});
|
||||
},
|
||||
dataRecybin: function (data = {}) {
|
||||
var formHtml = laytpl($('#dataRecybinTpl').html()).render(data || {});
|
||||
layer.open({
|
||||
title: '资源回收站',
|
||||
content: formHtml,
|
||||
anim: "slideLeft",
|
||||
offset: "r",
|
||||
area: ['60%', '99%'],
|
||||
shade: 0.1,
|
||||
shadeClose: true,
|
||||
success: function (layero, index) {
|
||||
layui.table.render({
|
||||
elem: '#dataRecybinTable',
|
||||
url: 'recyclebin',
|
||||
height: 'full-100',
|
||||
defaultToolbar: [{
|
||||
title: '批量删除数据',
|
||||
layEvent: 'dataDelete',
|
||||
icon: 'layui-icon-delete',
|
||||
onClick: function (obj) {
|
||||
var checkStatus = treeTable.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
|
||||
let ids = checkData.map((item, index, array) => { return item.id; });
|
||||
console.log(ids);
|
||||
}
|
||||
}, 'filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{ type: 'checkbox', fixed: 'left' },
|
||||
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
|
||||
{ field: 'title', title: 'title', width: 180, fixed: 'left' },
|
||||
{ field: 'name', title: 'name', width: 180, fixed: 'left' },
|
||||
{ field: 'route', title: '路由', minWidth: 200, sort: true },
|
||||
{
|
||||
fixed: "right", title: "操作", width: 120, align: "center", templet: function (d) {
|
||||
return `<div class="layui-btn-group">
|
||||
<a class="layui-btn layui-btn-sm" title="恢复数据" lay-event="restore" > <i class="layui-icon layui-icon-edit"></i> </a >
|
||||
<a class="layui-btn layui-btn-sm" title="删除数据" lay-event="delete"><i class="layui-icon layui-icon-delete"></i> </a>
|
||||
</div >`;
|
||||
}
|
||||
}
|
||||
]],
|
||||
page: true,
|
||||
done: function (res, curr, count, origin) {
|
||||
layui.table.on('tool(dataRecybinTable)', function (elem) {
|
||||
var data = elem.data;
|
||||
switch (elem.event) {
|
||||
case 'restore':
|
||||
active.dataRestore([data.id]);
|
||||
break;
|
||||
case 'delete':
|
||||
active.dataDelete([data.id], 1);
|
||||
break;
|
||||
}
|
||||
});
|
||||
layui.table.on('toolbar(dataRecybinTable)', function (elem) {
|
||||
switch (elem.event) {
|
||||
case 'dataDelete':
|
||||
var checkStatus = table.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
|
||||
let idss = checkData.map((item, index, array) => { return item.id; });
|
||||
active.dataDelete(idss, 1);
|
||||
break;
|
||||
case 'dataRestore':
|
||||
var checkStatus = table.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
|
||||
let ids = checkData.map((item, index, array) => { return item.id; });
|
||||
active.dataRestore(ids);
|
||||
break;
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
dataRestore: function (ids) {
|
||||
if (ids.length === 0) {
|
||||
return layer.msg('请选择数据');
|
||||
}
|
||||
http.put('restore', { ids: ids.join(',') })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
layer.msg('恢复成功');
|
||||
table.reload('dataTable', {}, true);
|
||||
} else {
|
||||
layer.msg(res.msg || '恢复失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,83 @@
|
||||
整体模板为动态layout模式
|
||||
前台 layout为 app\frontend\view\common\layout.html
|
||||
后台 layout为 app\backend\view\common\layout.html
|
||||
用户中心layout为 app\member\view\common\layout.html
|
||||
|
||||
插件会自动调用这三个 也可以在插件得初始化中禁用layout
|
||||
layui 目录 public\assets\layui
|
||||
ywxapp 目录 public\assets\ywxapp
|
||||
ywxapp中加载了 layui.http ueditor 等
|
||||
|
||||
table列表模板可以参考 docs\tpl\table.html
|
||||
树形列表模板可以参考 docs\tpl\treetable.html
|
||||
|
||||
目录划分
|
||||
├─app 应用目录
|
||||
│ ├─backend
|
||||
│ ├─member
|
||||
│ ├─frontend
|
||||
│ ├─api
|
||||
│ │ ├─controller 控制器目录
|
||||
│ │ ├─model 模型目录
|
||||
│ │ ├─......
|
||||
│ │ ├─common.php 公共函数文件
|
||||
│ │ └─event.php 事件定义文件
|
||||
│——addon
|
||||
│ ├─(插件名)
|
||||
│ │ ├─controller 控制器目录
|
||||
│ │ ├─model 模型目录
|
||||
│ │ ├─config 配置目录和应用类是
|
||||
│ │ ├─library 一些需要使用的类
|
||||
│ │ ├─route 路由
|
||||
│ │ │ ├─app.php
|
||||
│ │ ├─service 服务
|
||||
│ │ ├─listen 事件监听
|
||||
│ │ ├─subscribe 事件订阅
|
||||
│ │ ├─event 事件绑定
|
||||
│ │ ├─view
|
||||
│ │ ├─common.php 公共函数文件
|
||||
│ │ └─event.php 事件定义文件
|
||||
├─config 配置目录
|
||||
├─view 视图目录
|
||||
├─route 路由定义目录
|
||||
│ ├─route.php 路由定义文件
|
||||
│ └─ ...
|
||||
├─public WEB目录(对外访问目录)
|
||||
│ ├─assets
|
||||
│ │ ├─layui layui目录
|
||||
│ │ ├─ywxapp ywxapp目录 为layui加载http ueditor 等
|
||||
│ ├─static
|
||||
│ │ ├─(应用或插件名)
|
||||
│ │ │ ├─img 插件图片
|
||||
│ │ │ ├─css 插件样式
|
||||
│ │ │ ├─modules 插件js模块 参考layui模块定义
|
||||
│ │ │ ├─views 一些需要Ajax加载的页面
|
||||
│ ├─index.php 入口文件
|
||||
│ ├─router.php 快速测试文件
|
||||
|
||||
插件 目录
|
||||
├─(插件名)
|
||||
│ ├─controller 控制器目录
|
||||
│ │ ├─backend 插件后台控制器目录
|
||||
│ │ ├─member 插件用户中心控制器目录
|
||||
│ │ ├─api 插件api控制器
|
||||
│ │ ├─... 插件前台控制器可以直接放这里
|
||||
│ ├─model 模型目录
|
||||
│ ├─config 配置目目 参考thinkphp配置
|
||||
│ ├─library 一些需要使用的类
|
||||
│ ├─route 路由
|
||||
│ │ ├─app.php 基本就只需要这一个路由文件就行了 然后请参考下方插件路由定义
|
||||
│ ├─service 服务
|
||||
│ ├─listen 事件监听
|
||||
│ ├─subscribe 事件订阅
|
||||
│ ├─event 事件绑定
|
||||
│ ├─view
|
||||
│ │ ├─backend
|
||||
│ │ ├─member
|
||||
│ │ ├─frontend
|
||||
│ ├─common.php 公共函数文件
|
||||
│ └─event.php 事件定义文件
|
||||
|
||||
插件路由定义
|
||||
整体参考demo演示插件
|
||||
|
||||
+1
-1
@@ -244,7 +244,7 @@ Route::group('api', function () {
|
||||
|
||||
- 命名空间:
|
||||
- 后台控制器:`addon\<name>\controller\admin`,继承 `ywxapp\controller\AddonBackend`
|
||||
- API 控制器:`addon\<name>\controller\api`,继承 `ywxapp\controller\ApiController`
|
||||
- API 控制器:`addon\<name>\controller\api`,继承 `ywxapp\controller\ApiBase`
|
||||
- 模型:`addon\<name>\model\Xxx`
|
||||
- 服务:`addon\<name>\service\Xxx`
|
||||
- 鉴权开关(开发期可放开):
|
||||
|
||||
Reference in New Issue
Block a user