勾股OA 源码解读 — ThinkPHP 8 + Layui 企业OA系统
勾股OA 源码解读 — ThinkPHP 8 + Layui 企业OA系统
项目地址:https://gitee.com/gouguopen/office
本地路径:E:\code\hexo\office
技术栈:ThinkPHP 8.1 + Layui 2.x + MySQL 8.0 + PHP 8.2+
目录
- 项目总览
- 目录结构
- 技术栈与依赖
- ThinkPHP 8 多应用架构
- BaseController — 认证与权限拦截
- 登录流程精读
- RBAC 权限引擎 — Systematic.php
- 审批工作流 — Check.php
- 财务模块 — 标准 CRUD 范式
- 数据权限设计
- Layui 前端架构
- 工具函数库 — common.php
- 推荐学习路线
1. 项目总览
勾股OA(GouGuOA)是一套基于 ThinkPHP 8 + Layui 构建的企业级办公自动化系统,涵盖行政、财务、合同、客户、项目等完整业务闭环。项目采用多应用架构,共 18 个子应用、80+ 控制器、70+ 模型,代码量大但结构清晰,是学习 ThinkPHP 8 实战开发的优秀范例。
核心业务模块
| 应用 | 路径 | 功能说明 |
|---|---|---|
home | app/home/ | 首页、登录、权限规则、消息、审批中心、请假/外出/加班/出差 |
user | app/user/ | 员工管理、部门、岗位、劳动合同、关怀、奖惩 |
finance | app/finance/ | 报销、借支、开票、收款、付款、票据 |
contract | app/contract/ | 合同管理、采购、供应商、产品 |
customer | app/customer/ | 客户管理、联系人、商机、跟进记录 |
project | app/project/ | 项目管理、任务、文档、里程碑 |
adm | app/adm/ | 行政管理:印章、车辆、会议、公文、资产、公告 |
oa | app/oa/ | 日常办公:日程、计划、工作汇报、会议纪要 |
api | app/api/ | API接口:审批流程、评论、导入导出 |
base | app/base/ | 基础控制器和视图模板(不直接对外) |
qiye | app/qiye/ | 企业微信端 |
mobile | app/mobile/ | 移动端菜单配置 |
disk | app/disk/ | 网盘文件管理 |
crud | app/crud/ | 代码生成器 |
install | app/install/ | 安装向导 |
listener | app/listener/ | 事件监听器 |
exception | app/exception/ | 异常处理 |
2. 目录结构
office/├── app/ # 应用目录(ThinkPHP 8 多应用)│ ├── adm/ # 行政管理│ │ ├── controller/ # 15个控制器│ │ ├── model/ # 10个模型│ │ └── view/ # 视图模板│ ├── api/ # API接口│ │ ├── controller/ # Check.php(审批)、Comment、Export、Import...│ │ ├── BaseController.php # API基类(JWT认证)│ │ └── model/│ ├── base/ # 基础模块│ │ ├── BaseController.php # ★ 核心基类│ │ └── view/common/base.html # ★ 基础模板│ ├── contract/ # 合同管理│ ├── crud/ # 代码生成器│ ├── customer/ # 客户管理│ ├── disk/ # 网盘│ ├── finance/ # 财务管理│ │ ├── controller/ # Expense、Loan、Invoice...│ │ ├── model/│ │ ├── validate/│ │ └── view/│ ├── home/ # 主应用│ │ ├── controller/ # Login、Index、Rule、Role...│ │ ├── model/│ │ ├── validate/│ │ └── view/│ ├── install/ # 安装向导│ ├── listener/ # 事件监听器│ ├── mobile/ # 移动端│ ├── oa/ # 日常办公│ ├── project/ # 项目管理│ ├── qiye/ # 企业微信端│ ├── user/ # 用户管理│ ├── AppService.php # 应用服务│ ├── BaseController.php # 默认基类(空壳)│ ├── ExceptionHandle.php # 异常处理│ ├── Request.php # 请求对象│ ├── common.php # ★ 1679行工具函数库│ └── event.php # 事件定义├── config/ # 配置目录│ ├── app.php # ★ 应用配置│ ├── database.php # 数据库配置│ ├── cache.php # 缓存配置│ ├── session.php # Session配置│ ├── view.php # 视图配置│ └── ...├── extend/ # 扩展类库│ └── systematic/│ └── Systematic.php # ★ RBAC权限引擎├── public/ # Web根目录│ ├── index.php # 入口文件│ ├── tpl/ # 错误页面模板│ └── static/ # 静态资源│ ├── gougu/ # 勾股前端资源│ │ ├── gouguInit.js # ★ 前端入口│ │ ├── gougu/css/│ │ └── ...│ └── layui/ # Layui框架├── route/ # 路由│ └── app.php # 路由定义(极简,主要靠多应用自动路由)├── runtime/ # 运行时目录├── vendor/ # Composer依赖├── composer.json # 依赖配置└── think # 命令行入口3. 技术栈与依赖
composer.json 核心依赖
{ "require": { "php": ">=8.2", "ext-curl": "*", "ext-fileinfo": "*", "ext-openssl": "*", "ext-pdo": "*", "ext-zip": "*", "topthink/framework": "^8.1.2", // ThinkPHP 8 核心框架 "topthink/think-orm": "^4.0.5", // ORM "topthink/think-filesystem": "^3.0", // 文件系统 "topthink/think-multi-app": "^1.0", // ★ 多应用支持 "topthink/think-view": "^2.0", // 模板引擎 "topthink/think-captcha": "^3.0", // 验证码 "topthink/think-helper": "^3.1", // 助手函数 "topthink/think-image": "^1.0.8", // 图片处理 "overtrue/pinyin": "^5.2", // 拼音转换 "phpmailer/phpmailer": "^6.8", // 邮件发送 "firebase/php-jwt": "^7.0", // JWT认证 "phpoffice/phpspreadsheet": "^1.2", // Excel导入导出 "phpoffice/phpword": "^1.2", // Word生成 "mpdf/mpdf": "^8.1", // PDF生成 "alibabacloud/dysmsapi-20170525": "^4.3" // 阿里云短信 }}PSR-4 自动加载
{ "autoload": { "psr-4": { "app\\": "app" }, "psr-0": { "": "extend/" } }}关键点:extend/ 目录使用 PSR-0 自动加载,所以 extend/systematic/Systematic.php 中的类名空间 systematic\Systematic 可以直接全局使用。
4. ThinkPHP 8 多应用架构
4.1 自动多应用路由
勾股OA 使用 think-multi-app 实现多应用,URL 第一段自动映射到应用目录:
URL: /finance/expense/datalist ↓ ↓ ↓ 应用名 控制器 方法 ↓ ↓ ↓ app/finance Expense datalist()对应代码文件:
app/finance/controller/Expense.php → namespace app\finance\controller; → class Expense extends BaseController → public function datalist()4.2 配置文件
config/app.php 核心配置:
return [ 'app_host' => env('app.host', ''), 'app_namespace' => '', 'with_route' => true, // 启用路由 'default_app' => 'home', // 默认应用 'default_timezone' => 'Asia/Shanghai',
'default_filter' => 'htmlspecialchars', // 全局XSS过滤
'page_size' => 20, // 默认分页 'file_size' => 50, // 附件大小50M 'session_admin' => 'gougu_admin', // Session键名
'http_exception_template' => [ 401 => public_path() . 'tpl/401.html', 403 => public_path() . 'tpl/403.html', 404 => public_path() . 'tpl/404.html', 405 => public_path() . 'tpl/405.html', // 无权限 500 => public_path() . 'tpl/500.html', ]];4.3 数据库配置
config/database.php:
return [ 'default' => 'mysql', 'auto_timestamp' => true, // 自动写入时间戳 'datetime_format' => 'Y-m-d H:i:s', 'connections' => [ 'mysql' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'oa', 'username' => 'root', 'password' => '123456', 'hostport' => '3306', 'prefix' => 'oa_', // 表前缀 oa_ 'charset' => 'utf8mb4', 'fields_strict' => true, // 严格检查字段 ], ],];4.4 路由文件
route/app.php 极其简洁,几乎不定义路由,完全依赖多应用自动路由:
use think\facade\Route;
Route::get('think', function () { return 'hello,ThinkPHP6!';});
Route::get('hello/:name', 'index/hello');4.5 入口文件
public/index.php(标准 ThinkPHP 8 入口):
<?php// 调整应用目录结构(base模块不对外访问)namespace think;require __DIR__ . '/../vendor/autoload.php';
$http = (new App())->http;$response = $http->run();$response->send();$http->end($response);5. BaseController — 认证与权限拦截
文件:
app/base/BaseController.php
这是整个系统最重要的文件,所有 Web 端控制器都继承它。它在构造函数中完成完整的认证和权限检查链。
5.1 完整源码
namespace app\base;
use think\facade\Cache;use think\facade\Db;use think\facade\Request;use think\facade\Session;use think\facade\View;use systematic\Systematic;
abstract class BaseController{ protected $batchValidate = false; protected $pageSize = 20; protected $middleware = []; protected $module; protected $controller; protected $action; protected $uid; protected $did; // 部门ID protected $pid; // 岗位ID protected $model;
public function __construct() { // 解析当前请求的 模块/控制器/方法 $this->module = strtolower(app('http')->getName()); $this->controller = strtolower(Request::controller()); $this->action = strtolower(Request::action()); $this->uid = 0; $this->did = 0; $this->pid = 0; $this->initialize(); }
protected function initialize() { $this->checkLogin(); $this->pageSize = Request::param('limit', \think\facade\Config::get('app.page_size')); }
// ... checkLogin 和 checkAuth 见下文}5.2 认证流程(checkLogin)
protected function checkLogin(){ // 1. 登录页和验证码页跳过检查 if ($this->controller !== 'login' && $this->controller !== 'captcha') { $session_admin = get_config('app.session_admin');
// 2. 未登录 → 重定向到登录页 if (!Session::has($session_admin)) { if (request()->isAjax()) { return to_assign(404, '请先登录'); } else { redirect('/home/login/index.html')->send(); exit; } } else { // 3. 已登录 → 加载用户信息 $this->uid = Session::get($session_admin); $login_admin = get_admin($this->uid); $this->did = $login_admin['did']; $this->pid = $login_admin['pid']; $is_lock = $login_admin['is_lock'];
// 4. 10小时超时检查 $last_login_time = Db::name('Admin') ->where(['id' => $this->uid]) ->value('last_login_time'); $timeDiff = time() - $last_login_time; if ($timeDiff > 36000) { // 36000秒 = 10小时 Session::delete($session_admin); redirect('/home/login/index.html')->send(); exit; }
// 5. 更新最后活动时间(滑动会话) Db::name('Admin') ->where(['id' => $this->uid]) ->update(['last_login_time' => time()]);
// 6. 锁屏检查 if ($is_lock == 1) { redirect('/home/login/lock.html')->send(); exit; }
// 7. 将用户信息注入视图 View::assign('login_admin', $login_admin);
// 8. 部分控制器跳过权限检查 $not_check = ['index','leaves','outs','overtimes','trips','message']; if ($this->module == 'home' && in_array($this->controller, $not_check)) { return true; } else { // 9. 强制修改初始密码 $regPwd = $login_admin['reg_pwd']; if ($regPwd !== '') { redirect('/home/index/edit_password.html')->send(); exit; }
// 10. RBAC 权限检查 if (!$this->checkAuth()) { if (request()->isAjax()) { return to_assign(405, '你没有权限'); } else { redirect('/home/index/role')->send(); exit; } } } } }}5.3 权限检查(checkAuth)
protected function checkAuth(){ $uid = $this->uid; $GOUGU = new Systematic(); $GOUGU->auth($uid); // 构建/读取权限缓存
$auth_list = Cache::get('RulesSrc' . $uid); // 当前用户权限 $pathUrl = $this->module . '/' . $this->controller . '/' . $this->action;
if (!in_array($pathUrl, $auth_list)) { return false; // 无权限 } return true; // 有权限}5.4 认证流程图
请求进入 │ ├─ controller == login/captcha? ──→ 跳过检查 │ ├─ Session 存在? ──No──→ 重定向登录页 │ ├─ 加载用户信息 (uid, did, pid) │ ├─ 超过10小时? ──Yes──→ 清除Session,重定向登录页 │ ├─ 更新 last_login_time(滑动会话) │ ├─ is_lock == 1? ──Yes──→ 重定向锁屏页 │ ├─ 注入 login_admin 到视图 │ ├─ 控制器在白名单? ──Yes──→ 通过 │ ├─ reg_pwd 非空? ──Yes──→ 重定向修改密码页 │ └─ RBAC checkAuth() ──Fail──→ 405无权限 └─Pass──→ 进入控制器方法5.5 API 基类
app/api/BaseController.php 是 API 端的基类,与 Web 端不同:
- 无 RBAC 权限检查(API 权限由业务自行控制)
- 无锁屏、超时检查
- 支持 JWT 配置(默认 2 小时过期)
- 统一 JSON 响应(
apiSuccess/apiError/apiReturn)
abstract class BaseController{ protected $jwt_conf = [ 'secrect' => 'gouguoa', 'iss' => 'www.gougucms.com', 'aud' => 'gouguoa', 'exptime' => 7200, // 2小时过期 ];
protected function apiReturn($data, int $code = 0, $msg = '', string $type = '', array $header = []): Response { $result = [ 'code' => $code, 'msg' => $msg, 'time' => time(), 'data' => $data, ]; $response = Response::create($result, $type ?: 'json')->header($header); throw new HttpResponseException($response); }}6. 登录流程精读
文件:
app/home/controller/Login.php
6.1 登录页展示
class Login{ public function index() { // 检测企业微信环境 $wxwork = is_wxwork(); $mobile = is_mobile(); if ($wxwork) { return redirect('/qiye/login/login'); // 企业微信端 } if ($mobile) { return redirect('/qiye/login/index'); // 移动端 } return View(); // PC端登录页 }}注意:Login 类不继承 BaseController,因为登录页不需要认证。这是唯一不继承 BaseController 的控制器。
6.2 登录提交
public function login_submit(){ $param = get_params();
// 1. 表单验证 try { validate(UserCheck::class)->check($param); } catch (ValidateException $e) { return to_assign(1, $e->getError()); }
// 2. 支持用户名或手机号登录 $admin = Db::name('Admin') ->where(['username' => $param['username'], 'delete_time' => 0]) ->find(); if (empty($admin)) { // 尝试用手机号查找 $admin = Db::name('Admin') ->where(['mobile' => $param['username'], 'delete_time' => 0]) ->find(); if (empty($admin)) { return to_assign(1, '用户名或手机号码错误'); } }
// 3. 密码验证(salt + md5) $param['pwd'] = set_password($param['password'], $admin['salt']); if ($admin['pwd'] !== $param['pwd']) { return to_assign(1, '用户或密码错误'); }
// 4. 状态检查 if ($admin['status'] != 1) { return to_assign(1, '该用户禁止登录'); }
// 5. 更新登录信息 $data = [ 'is_lock' => 0, 'last_login_time' => time(), 'last_login_ip' => request()->ip(), 'login_num' => $admin['login_num'] + 1, ]; Db::name('admin')->where(['id' => $admin['id']])->update($data);
// 6. 写入 Session $session_admin = get_config('app.session_admin'); Session::set($session_admin, $admin['id']);
// 7. 生成 Token(用于 API 调用) $token = make_token(); set_cache($token, $admin, 7200);
// 8. 记录登录日志 $logdata = [ 'uid' => $admin['id'], 'type' => 'login', 'action' => '登录', 'subject' => '系统', 'param_id' => $admin['id'], 'param' => '[]', 'ip' => request()->ip(), 'create_time' => time() ]; Db::name('AdminLog')->strict(false)->field(true)->insert($logdata);
return to_assign(0, '登录成功', ['uid' => $admin['id']]);}6.3 锁屏与解锁
public function lock(){ $session_admin = get_config('app.session_admin'); $admin_id = Session::get($session_admin); $admin = Db::name('admin')->where(['id' => $admin_id])->find();
if (request()->isAjax()) { // 解锁:验证密码 $param = get_params(); $pwd = set_password($param['lock_password'], $admin['salt']); if ($admin['pwd'] !== $pwd) { return to_assign(1, '密码错误'); } Db::name('admin')->where('id', $admin['id']) ->update(['is_lock' => 0]); return to_assign(0, '解锁成功', ['uid' => $admin['id']]); }
// 锁屏:设置 is_lock = 1 Db::name('admin')->where('id', $admin['id']) ->update(['is_lock' => 1]); return View();}6.4 登录流程图
用户输入用户名/密码 │ ▼ UserCheck 验证器校验 │ ▼ 查询 Admin 表(用户名 OR 手机号) │ ├─ 不存在 → "用户名或手机号码错误" │ ▼ set_password(password, salt) 加密比对 │ ├─ 不匹配 → "用户或密码错误" │ ▼ status != 1 → "禁止登录" │ ▼ 更新 last_login_time, login_num, ip │ ▼ Session::set('gougu_admin', $admin_id) │ ▼ 生成 Token,缓存 7200 秒 │ ▼ 记录 AdminLog 登录日志 │ ▼ 返回 {code:0, msg:'登录成功', data:{uid}}7. RBAC 权限引擎 — Systematic.php
文件:
extend/systematic/Systematic.php
7.1 权限模型设计
勾股OA 的 RBAC 采用 岗位 → 角色组 → 权限规则 三级模型:
用户(Admin) │ ├── position_id → 岗位(Position) │ │ │ └── PositionGroup → 角色组(AdminGroup) │ │ │ └── rules (逗号分隔的规则ID) │ │ │ └── AdminRule │ ├── src: "finance/expense/datalist" │ ├── title: "报销列表" │ └── ... │ ├── did → 部门(Department) └── pid → 岗位ID7.2 核心代码
namespace systematic;
use think\facade\Config;use think\facade\Cache;use think\facade\Db;
class Systematic{ public function auth($uid) { // 缓存未命中时重建 if (!Cache::get('RulesSrc' . $uid) || !Cache::get('RulesSrc0')) {
// 1. 获取用户岗位 $position_id = Db::name('Admin') ->where('id', $uid) ->value('position_id');
// 2. 通过岗位 → 岗位角色组关联表 → 获取角色组 $groups = Db::name('PositionGroup') ->alias('a') ->join("AdminGroup g", "a.group_id=g.id", 'LEFT') ->where([ ['a.pid', '=', $position_id], ['g.status', '=', 1] ]) ->select()->toArray();
// 3. 合并所有角色组的权限规则ID $ids = []; foreach ($groups as $g) { $ids = array_merge($ids, explode(',', trim($g['rules'], ','))); } $ids = array_unique($ids);
// 4. 读取所有权限规则(用于超级管理员判断) $rules_all = Db::name('AdminRule') ->field('src')->select()->toArray();
// 5. 读取当前用户的权限规则 $rules = Db::name('AdminRule') ->where('id', 'in', $ids) ->field('src')->select()->toArray();
// 6. 提取规则路径(小写化) $auth_list_all = []; $auth_list = []; foreach ($rules_all as $rule_all) { $auth_list_all[] = strtolower($rule_all['src']); } foreach ($rules as $rule) { $auth_list[] = strtolower($rule['src']); }
// 7. 缓存 10 小时(36000秒) Cache::tag('adminRules')->set('RulesSrc0', $auth_list_all, 36000); Cache::tag('adminRules')->set('RulesSrc' . $uid, $auth_list, 36000); } }}7.3 权限匹配机制
在 BaseController::checkAuth() 中:
$pathUrl = $this->module . '/' . $this->controller . '/' . $this->action;// 例如: "finance/expense/datalist"
if (!in_array($pathUrl, $auth_list)) { return false; // 无权限}return true; // 有权限权限规则表 oa_admin_rule 中的 src 字段存储路径,格式为 模块/控制器/方法,与 URL 路径一一对应。
7.4 关键设计要点
| 要点 | 说明 |
|---|---|
| 权限缓存 | 使用 Cache::tag('adminRules') 缓存,有效期 10 小时,与登录超时一致 |
| 超级管理员 | 拥有所有权限的用户,其 RulesSrc{uid} 等于 RulesSrc0(全部规则) |
| 权限粒度 | 精确到 方法级别(module/controller/action) |
| 权限更新 | 修改角色组权限后需清缓存,系统通过 Cache::tag('adminRules')->clear() 实现 |
8. 审批工作流 — Check.php
文件:
app/api/controller/Check.php(654行,系统最复杂的文件)
这是勾股OA 最核心的业务逻辑,一套自研审批工作流引擎,支持自由审批、固定流程、会签、或签、回退、反确认等模式。
8.1 数据模型
FlowCate (流程分类) ├── name: "expense_check" ← 业务标识 ├── title: "报销审批" ├── check_table: "Expense" ← 关联的业务表名 ├── template_id: 1 ← 消息模板ID ├── is_back: 1 ← 是否允许回退 ├── is_reversed: 1 ← 是否允许反确认 └── is_copy: 1 ← 是否允许抄送
Flow (审批流程) ├── cate_id → FlowCate.id ├── flow_list: serialize(...) ← 序列化的审批步骤配置 └── department_ids: "1,2,3" ← 适用部门(空=全部)
FlowStep (审批步骤实例) ├── action_id: 123 ← 业务记录ID ├── flow_id: 1 ← 流程ID ├── sort: 0,1,2... ← 步骤顺序 ├── check_role: 1-5 ← 审批角色类型 ├── check_types: 1会签 / 2或签 ├── check_uids: "1,2,3" ← 当前步骤审批人 └── check_position_id: 5 ← 岗位ID(check_role=3时)
FlowRecord (审批记录) ├── action_id: 123 ├── check_table: "Expense" ├── step_id → FlowStep.id ├── check_uid: 1 ← 审批人 ├── check_status: 0提交/1通过/2拒绝/3撤回/4反确认 ├── check_files: "1,2" ← 附件 └── content: "同意" ← 审批意见8.2 审批角色类型(check_role)
| 值 | 含义 | 审批人确定方式 |
|---|---|---|
| 0 | 自由审批 | 提交时手动指定 |
| 1 | 当前部门负责人 | get_department_leader($uid) |
| 2 | 上级部门负责人 | get_department_leader($uid, 1) |
| 3 | 指定岗位 | 查询 Admin 表中该岗位的在职人员 |
| 4 | 指定人员 | 预先配置 |
| 5 | 指定人员(可回退) | 预先配置,拒绝时退回上一步 |
8.3 提交审批(submit_check)
public function submit_check(){ $param = get_params(); $flow_cate = Db::name('FlowCate') ->where(['name' => $param['check_name']])->find(); $flow_list = Db::name('Flow') ->where('id', $param['flow_id'])->value('flow_list'); $flow = unserialize($flow_list);
$check_table = $flow_cate['check_table'];
// 1. 删除原有的审批步骤和记录(重新提交场景) Db::name('FlowStep') ->where(['action_id' => $param['action_id'], 'flow_id' => $param['flow_id']]) ->update(['delete_time' => time()]); Db::name('FlowRecord') ->where(['action_id' => $param['action_id'], 'check_table' => $check_table]) ->update(['delete_time' => time()]);
// 2. 创建提交记录 $recordData = [ 'action_id' => $param['action_id'], 'check_table' => $check_table, 'step_id' => 0, 'check_uid' => $this->uid, 'check_status' => 0, // 提交 'content' => '提交申请', 'create_time' => time() ];
if (!isset($param['check_uids'])) { // ===== 固定流程模式 ===== $step = []; $sort = 0; foreach ($flow as $value) { // 根据 check_role 解析审批人 if ($value['check_role'] == 1) { $value['check_uids'] = get_department_leader($this->uid); $value['flow_name'] = '当前部门负责人'; } if ($value['check_role'] == 2) { $value['check_uids'] = get_department_leader($this->uid, 1); $value['flow_name'] = '上级部门负责人'; } if ($value['check_role'] == 3) { $check_uids = Db::name('Admin') ->where(['position_id' => $value['check_position_id'], 'status' => 1]) ->column('id'); $value['check_uids'] = implode(',', $check_uids); $value['flow_name'] = $check_position; } // ... check_role 4, 5 略
if (!empty($value['check_uids'])) { $step[] = [ 'action_id' => $param['action_id'], 'flow_id' => $param['flow_id'], 'check_uids' => $value['check_uids'], 'check_role' => $value['check_role'], 'check_types' => $value['check_types'], 'sort' => $sort ]; $sort++; } }
// 3. 批量插入审批步骤 Db::name('FlowStep')->insertAll($step);
// 4. 更新业务表状态 Db::name($check_table)->where('id', $param['action_id'])->update([ 'check_flow_id' => $param['flow_id'], 'check_status' => 1, // 审批中 'check_step_sort' => 0, // 当前步骤 'check_uids' => $step[0]['check_uids'], // 当前审批人 'check_copy_uids' => $param['check_copy_uids'] ?? '' ]);
// 5. 发送消息通知(事件机制) if ($flow_cate['template_id'] > 0) { event('SendMessage', [ 'from_uid' => $this->uid, 'to_uids' => $step[0]['check_uids'], 'template_id' => $flow_cate['template_id'], 'content' => ['action_id' => $param['action_id'], 'title' => $subject] ]); } } else { // ===== 自由审批模式 ===== // 直接使用提交者指定的审批人 Db::name('FlowStep')->insert([ 'action_id' => $param['action_id'], 'flow_id' => $param['flow_id'], 'check_uids' => $param['check_uids'], 'flow_name' => '自由审批', ]); // ... 更新业务表 + 发送通知 }}8.4 审批通过(flow_check — check=1)
public function flow_check(){ $param = get_params(); $detail = Db::name($check_table)->where('id', $action_id)->find(); $step = Db::name('FlowStep') ->where(['action_id' => $action_id, 'sort' => $detail['check_step_sort']]) ->find();
if ($param['check'] == 1) { // === 审批通过 ===
// 1. 验证当前用户是否有审批权限 $check_uids = explode(",", $detail['check_uids']); if (!in_array($this->uid, $check_uids)) { return to_assign(1, '您没权限审核该审批'); }
if ($step['check_role'] == 0) { // 自由人审批 if ($param['check_node'] == 2) { // 继续下一步(手动指定) $next_step = $detail['check_step_sort'] + 1; // ... 插入新步骤 } else { // 审核结束 $param['check_status'] = 2; // 审核完成 } } else { // 固定流程审批 $check_count = Db::name('FlowRecord') ->where(['step_id' => $step['id']])->count(); $flow_count = explode(',', $step['check_uids']);
// 从当前审批人中移除自己 $new_uids = array_diff($check_uids, [$this->uid]); $param['check_uids'] = implode(',', $new_uids);
// 判断是否本步骤所有人审批完成 // check_types=1: 会签(所有人通过才算通过) // check_types=2: 或签(任一人通过即通过) if ((($check_count + 1) >= count($flow_count) && $step['check_types'] == 1) || $step['check_types'] == 2) {
// 查找下一步 $next_step = Db::name('FlowStep') ->where(['sort' => $detail['check_step_sort'] + 1]) ->find();
if ($next_step) { // 存在下一步 → 解析下一步审批人 if ($next_step['check_role'] == 1) { $param['check_uids'] = get_department_leader($detail['admin_id']); } elseif ($next_step['check_role'] == 2) { $param['check_uids'] = get_department_leader($detail['admin_id'], 1); } elseif ($next_step['check_role'] == 3) { $uids = Db::name('Admin') ->where(['position_id' => $next_step['check_position_id']]) ->column('id'); $param['check_uids'] = implode(',', $uids); } else { $param['check_uids'] = $next_step['check_uids']; } $param['check_step_sort'] = $detail['check_step_sort'] + 1; $param['check_status'] = 1; // 仍审批中 } else { // 不存在下一步 → 审核结束 $param['check_status'] = 2; // 审核完成 $param['check_uids'] = ''; } } else { // 本步骤还有人未审批 → 不发送消息 $param['send_msg'] = 0; } }
// 2. 添加历史审核人 $param['check_history_uids'] = empty($detail['check_history_uids']) ? $this->uid : $detail['check_history_uids'] . ',' . $this->uid;
// 3. 更新业务表 Db::name($check_table)->where('id', $action_id)->update($param);
// 4. 记录审批日志 Db::name('FlowRecord')->insert([ 'action_id' => $action_id, 'check_uid' => $this->uid, 'check_status' => 1, // 通过 'content' => $param['content'], 'check_files' => $param['check_files'], 'check_time' => time(), ]);
// 5. 发送消息通知(审批中→下一审批人 / 审核完成→提交人+抄送人) if ($param['check_status'] == 1 && $param['send_msg'] == 1) { event('SendMessage', [...]); // 通知下一审批人 } if ($param['check_status'] == 2) { event('SendMessage', [...]); // 通知提交人:审核通过 if (!empty($detail['check_copy_uids'])) { event('SendMessage', [...]); // 通知抄送人 } } }
// check=2: 审批拒绝(支持 check_role=5 回退到上一步) // check=3: 审批撤回(仅提交人可操作) // check=4: 审批反确认(打回重新提交)}8.5 审批状态流转
┌──────────────────────────────────────────────┐ │ │ ▼ │ 0(待提交) ──提交──→ 1(审批中) ──通过──→ 2(审核完成) │ │ │ │ │ ├──拒绝──→ 3(审核拒绝) │ │ │ │ │ │ │ └─check_role=5──→ │ │ │ 回退上一步 │ │ │ │ │ └──撤回──→ 4(撤回) ──反确认──→ 0(待提交) │ └──反确认──────────────────────────────────────→ 0(待提交)8.6 工作流与业务表的关系
审批工作流通过 check_name + action_id 与业务表完全解耦:
业务表 (如 Expense) ├── id (action_id) ├── check_status ← 审批状态 (0/1/2/3/4) ├── check_flow_id ← 流程ID ├── check_step_sort ← 当前步骤序号 ├── check_uids ← 当前审批人 (逗号分隔) ├── check_history_uids ← 历史审批人 └── check_copy_uids ← 抄送人
FlowCate └── check_table: "Expense" ← 指向业务表名任何业务表只需添加上述审批字段,配置对应的 FlowCate + Flow,即可接入审批工作流,无需修改业务代码。
9. 财务模块 — 标准 CRUD 范式
控制器:
app/finance/controller/Expense.php
模型:app/finance/model/Expense.php
财务模块的报销管理是理解整个项目 CRUD 范式的最佳范例。
9.1 控制器结构
namespace app\finance\controller;
use app\base\BaseController;use app\finance\model\Expense as ExpenseModel;use app\finance\validate\ExpenseValidate;use think\exception\ValidateException;use think\facade\Db;use think\facade\View;
class Expense extends BaseController{ protected $model;
public function __construct() { parent::__construct(); // 触发认证和权限检查 $this->model = new ExpenseModel(); }
// 数据列表 public function datalist() { ... }
// 添加/编辑 public function add() { ... }
// 查看详情 public function view($id) { ... }
// 删除 public function del() { ... }
// 报销记录 public function record() { ... }}9.2 datalist — 多Tab数据过滤
public function datalist(){ $param = get_params(); if (request()->isAjax()) { $tab = isset($param['tab']) ? $param['tab'] : 0; $uid = $this->uid; $where = []; $whereOr = []; $where[] = ['delete_time', '=', 0]; // 逻辑删除过滤
if ($tab == 0) { // 全部(根据数据权限过滤) $auth = isAuthExpense($uid); if ($auth == 0) { // 非管理员只能看自己相关的 $whereOr[] = ['admin_id', '=', $this->uid]; $whereOr[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_uids)")]; $whereOr[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_history_uids)")]; $whereOr[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_copy_uids)")]; $dids = array_merge( get_leader_departments($uid), get_role_departments($uid) ); if (!empty($dids)) { $whereOr[] = ['did', 'in', $dids]; } } } if ($tab == 1) { // 我创建的 $where[] = ['admin_id', '=', $this->uid]; } if ($tab == 2) { // 待我审核的 $where[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_uids)")]; } if ($tab == 3) { // 我已审核的 $where[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_history_uids)")]; } if ($tab == 4) { // 抄送给我的 $where[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_copy_uids)")]; } if ($tab == 5) { // 已打款的 $where[] = ['pay_status', '=', 1]; }
// 时间范围搜索 if (!empty($param['diff_time'])) { $diff_time = explode('~', $param['diff_time']); $where[] = ['income_month', 'between', [ strtotime(urldecode($diff_time[0])), strtotime(urldecode($diff_time[1] . ' 23:59:59')) ]]; }
$list = $this->model->datalist($param, $where, $whereOr); return table_assign(0, '', $list); } else { return view(); // 非Ajax请求返回HTML页面 }}9.3 add — 添加/编辑合一 + 借支冲抵
public function add(){ $param = get_params(); if (request()->isAjax()) { // 时间戳转换 $param['income_month'] = strtotime(urldecode($param['income_month'])); $param['expense_time'] = strtotime(urldecode($param['expense_time']));
// 计算报销总金额(多条报销明细) $cost = 0; foreach ($param['amount'] as $value) { if ($value == 0) { return to_assign(1, '金额不能为零'); } $cost += $value; }
$param['admin_id'] = $this->uid; $param['did'] = $this->did; $param['cost'] = $cost; $param['pay_amount'] = $cost;
if (!empty($param['id']) && $param['id'] > 0) { // ===== 编辑 ===== validate(ExpenseValidate::class)->scene('edit')->check($param);
// 借支冲抵计算 if (!empty($param['loan_id'])) { $loan = Db::name('Loan')->where('id', $param['loan_id'])->find(); // 查询该借支已冲抵的总额 $balance_cost = Db::name('Expense') ->where([ ['delete_time', '=', 0], ['loan_id', '=', $param['loan_id']], ['id', '<>', $param['id']] ])->sum('balance_cost');
// ★ *100/100 避免浮点精度问题 $un_balance_cost = ($loan['cost'] * 100 - $balance_cost * 100) / 100;
if ($un_balance_cost < 0) { return to_assign(1, '所选借支已经冲抵完毕'); }
$balance = $un_balance_cost * 100 - $cost * 100; if ($balance >= 0) { // 全额冲抵 $param['balance_cost'] = $cost; $param['pay_amount'] = 0; } else { // 部分冲抵 $param['balance_cost'] = $un_balance_cost; $param['pay_amount'] = ($cost * 100 - $un_balance_cost * 100) / 100; } }
$this->model->edit($param); } else { // ===== 新增 ===== validate(ExpenseValidate::class)->scene('add')->check($param); // ... 同样的借支冲抵逻辑 $this->model->add($param); } } else { // 渲染表单页面 $id = isset($param['id']) ? $param['id'] : 0; View::assign('expense_cate', Db::name('ExpenseCate') ->where(['status' => 1])->select()->toArray());
if ($id > 0) { $detail = $this->model->getById($id); View::assign('detail', $detail); return view('edit'); } return view(); }}9.4 Model — 五件套范式
所有 Model 都遵循统一的 datalist / add / edit / getById / delById 五件套范式:
namespace app\finance\model;
use think\model;use think\facade\Db;
class Expense extends Model{ // 1. 分页列表 public function datalist($param = [], $where = [], $whereOr = []) { $rows = empty($param['limit']) ? get_config('app.page_size') : $param['limit']; $order = empty($param['order']) ? 'id desc' : $param['order'];
try { $list = self::where($where) ->where(function ($query) use ($whereOr) { if (!empty($whereOr)) { $query->whereOr($whereOr); } }) ->order($order) ->paginate(['list_rows' => $rows]) ->each(function ($item, $key) { // 数据加工:关联查询、格式化 $item->check_status_str = check_status_name($item->check_status); $item->admin_name = Db::name('Admin') ->where(['id' => $item->admin_id])->value('name'); $item->department = Db::name('Department') ->where(['id' => $item->did])->value('title'); $item->create_time = to_date($item->create_time); // ... }); return $list; } catch (\Exception $e) { return ['code' => 1, 'data' => [], 'msg' => $e->getMessage()]; } }
// 2. 新增 public function add($param) { try { $param['create_time'] = time(); $insertId = self::strict(false)->field(true)->insertGetId($param);
// 处理子表(报销明细) if (isset($param['amount'])) { foreach ($param['amount'] as $key => $value) { Db::name('ExpenseInterfix')->insert([ 'exid' => $insertId, 'amount' => $value, 'cate_id' => $param['cate_id'][$key], 'remarks' => $param['remarks'][$key], 'create_time' => time(), ]); } }
// 更新借支冲抵 if (!empty($param['loan_id'])) { $this->update_loan($param['loan_id']); }
add_log('add', $insertId, $param); // 操作日志 } catch (\Exception $e) { return to_assign(1, '操作失败:' . $e->getMessage()); } return to_assign(0, '操作成功', ['return_id' => $insertId]); }
// 3. 编辑 public function edit($param) { try { $param['update_time'] = time(); self::where('id', $param['id'])->strict(false)->field(true)->update($param); // ... 处理子表更新 add_log('edit', $param['id'], $param); } catch (\Exception $e) { return to_assign(1, '操作失败:' . $e->getMessage()); } return to_assign(0, '操作成功', ['return_id' => $param['id']]); }
// 4. 获取详情 public function getById($id) { $info = self::find($id); // 关联查询补充字段 $info['admin_name'] = Db::name('Admin') ->where(['id' => $info['admin_id']])->value('name'); $info['department'] = Db::name('Department') ->where(['id' => $info['did']])->value('title'); // 查询子表数据 $info['list'] = Db::name('ExpenseInterfix') ->alias('a') ->join('ExpenseCate c', 'a.cate_id = c.id', 'LEFT') ->where(['a.exid' => $info['id']]) ->select(); return $info; }
// 5. 删除(逻辑删除) public function delById($id, $type = 0) { if ($type == 0) { // 逻辑删除 self::where('id', $id)->update(['delete_time' => time()]); add_log('delete', $id); } else { // 物理删除 self::destroy($id); } return to_assign(); }
// 借支冲抵更新 public function update_loan($id) { $balance_cost = Db::name('Expense') ->where(['delete_time' => 0, 'loan_id' => $id]) ->sum('balance_cost'); $loan = Db::name('Loan')->where('id', $id)->find();
if ($balance_cost * 100 == $loan['cost'] * 100) { // 全额冲抵 Db::name('Loan')->where('id', $id)->update([ 'balance_cost' => $balance_cost, 'balance_status' => 2, 'back_status' => 2 ]); } // ... 部分冲抵、未冲抵 }}9.5 关键设计模式
| 模式 | 实现 | 说明 |
|---|---|---|
| Ajax 双模式 | isAjax() ? JSON : View() | 同一个方法处理数据请求和页面渲染 |
| 逻辑删除 | delete_time 字段 | 删除时写入时间戳,查询时 where delete_time=0 |
| 子表处理 | ExpenseInterfix | 主表+子表一对多,foreach 循环插入 |
| 操作日志 | add_log() | 统一记录操作类型和参数 |
| 表单验证 | ExpenseValidate | 使用 ThinkPHP 验证器场景验证 |
| 浮点精度 | *100/100 | 金额计算先乘100转整数再除回,避免浮点误差 |
10. 数据权限设计
勾股OA 的数据权限通过 auth_did 字段 + FIND_IN_SET 实现。
10.1 数据权限级别
| auth_did 值 | 含义 | 数据范围 |
|---|---|---|
| 0 | 个人 | 仅自己创建的数据 |
| 1 | 本部门 | 自己部门的数据 |
| 2 | 本部门及下级 | 本部门+子部门数据 |
| 10 | 全部 | 所有数据(管理员) |
10.2 实现方式
// 在 datalist 中根据权限构建查询条件$auth = isAuthExpense($uid); // 返回数据权限级别if ($auth == 0) { // 个人权限:只能看自己相关 $whereOr[] = ['admin_id', '=', $this->uid]; $whereOr[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_uids)")]; $whereOr[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_history_uids)")]; $whereOr[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_copy_uids)")];
// 如果是部门负责人,可以看部门数据 $dids = array_merge( get_leader_departments($uid), // 作为负责人的部门 get_role_departments($uid) // 作为角色管理的部门 ); if (!empty($dids)) { $whereOr[] = ['did', 'in', $dids]; }}// auth == 10 时不加额外条件,可看全部10.3 FIND_IN_SET 的使用
FIND_IN_SET 是 MySQL 内置函数,用于在逗号分隔的字符串中查找值:
-- 查找 check_uids 字段中包含 uid=5 的记录SELECT * FROM oa_expense WHERE FIND_IN_SET('5', check_uids);在 ThinkPHP 8 中使用 Db::raw() 表达式:
$where[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_uids)")];11. Layui 前端架构
11.1 基础模板继承
文件:
app/base/view/common/base.html
所有页面通过 ThinkPHP 模板继承复用基础结构:
<!DOCTYPE html><html lang="zh"><head> <meta charset="utf-8" /> <title>{:get_system_config('web','admin_title')}</title> <!-- CSS区块 --> {block name="css"} <link rel="stylesheet" href="{__GOUGU__}/gougu/css/gougu.css?v={:get_system_config('system','version')}"> {/block} {block name="style"}{/block}
<!-- 全局JS变量 --> <script> const login_admin = {$login_admin.id}; </script></head><body class="main-body"> <!-- 主体区块 --> {block name="body"}{/block}
<!-- 底部 --> {block name="footer"}{/block}
<!-- 脚本区块 --> {block name="script"}{/block}
<!-- 全局脚本 --> <script src="{__GOUGU__}/layui/layui.js"></script> <script src="{__GOUGU__}/gougu/gouguInit.js?v={:get_system_config('system','version')}"></script></body></html>11.2 子页面继承示例
app/finance/view/expense/datalist.html:
{extend name="../../base/view/common/base" /}
{block name="body"}<div class="p-page"> <!-- Tab 切换栏 --> <div class="layui-tab layui-tab-brief" lay-filter="tab"> <ul class="layui-tab-title"> <li class="layui-this">全部</li> <li>我申请的</li> <li>待我审批</li> <li>我已审批</li> <li>抄送我的</li> <li>已打款</li> </ul> </div>
<!-- 搜索表单 --> <form class="layui-form" id="barsearchform"> <input type="text" id="diff_time" placeholder="选择时间区间"> <select name="check_status">...</select> <button lay-submit lay-filter="table-search">搜索</button> </form>
<!-- 数据表格 --> <table id="table_expense" lay-filter="table_expense"></table></div>
<!-- 工具栏模板 --><script type="text/html" id="toolbarDemo"> <button class="layui-btn tool-add" data-href="/finance/expense/add"> + 添加报销申请 </button></script>{/block}
{block name="script"}<script> // 模块声明 const moduleInit = ['tool', 'tablePlus', 'laydatePlus'];
// ★ gouguInit 是全局入口函数 function gouguInit() { var table = layui.tablePlus, tool = layui.tool, laydatePlus = layui.laydatePlus;
// Tab 切换 element.on('tab(tab)', function(data) { $('[name="tab"]').val(data.index); layui.pageTable.reload({ where: { tab: data.index }, page: { curr: 1 } }); });
// 表格渲染 layui.pageTable = table.render({ elem: "#table_expense", toolbar: "#toolbarDemo", url: "/finance/expense/datalist", page: true, limit: 20, height: 'full-154', cols: [[ { field: 'id', title: 'ID', width: 80 }, { field: 'cost', title: '报销金额', width: 110 }, { field: 'check_status', title: '审批状态', templet: function(d) { return '<span class="check-status-color-' + d.check_status + '">『' + d.check_status_str + '』</span>'; } }, { field: 'right', title: '操作', fixed: 'right', templet: function(d) { var html = '<div class="layui-btn-group">'; html += '<span lay-event="view">详情</span>'; if ((d.check_status == 0 || d.check_status == 4) && d.admin_id == login_admin) { html += '<span lay-event="edit">编辑</span>'; html += '<span lay-event="del">删除</span>'; } html += '</div>'; return html; } } ]] });
// 行事件 table.on('tool(table_expense)', function(obj) { var data = obj.data; if (obj.event === 'view') { tool.side("/finance/expense/view?id=" + data.id); } if (obj.event === 'edit') { tool.side("/finance/expense/add?id=" + data.id); } if (obj.event === 'del') { layer.confirm('确定删除?', function(index) { tool.delete("/finance/expense/del", { id: data.id }, function(e) { if (e.code == 0) obj.del(); }); layer.close(index); }); } }); }</script>{/block}11.3 前端核心组件
| 组件 | 说明 |
|---|---|
gouguInit() | 页面入口函数,类似 DOMContentLoaded,所有页面逻辑从这里开始 |
moduleInit | 声明当前页面需要的 Layui 扩展模块 |
layui.tablePlus | 增强版表格组件(基于 Layui table 封装) |
layui.tool | 工具集:tool.side() 侧边栏打开、tool.delete() 删除请求 |
layui.laydatePlus | 增强版日期选择器 |
layui.pageTable | 全局表格实例引用,用于 Tab 切换时 reload() |
login_admin | 全局 JS 变量,当前登录用户 ID |
11.4 前端交互模式
页面加载 │ ▼gouguInit.js 自动执行 │ ├─ 加载 moduleInit 声明的模块 │ ├─ 调用 gouguInit() 函数 │ ├─ table.render() 渲染表格 │ │ │ └─ Ajax 请求 url → 后端 datalist() │ │ │ └─ 返回 JSON {code:0, data:[...], count:N} │ ├─ Tab 切换 → table.reload({where:{tab:N}}) │ ├─ 搜索按钮 → table.reload({where: 表单数据}) │ └─ 行操作 ├─ view → tool.side() 侧边栏打开详情 ├─ edit → tool.side() 侧边栏打开编辑 └─ del → layer.confirm() → tool.delete() → obj.del()12. 工具函数库 — common.php
文件:
app/common.php(1679 行)
这是项目的工具函数大全,包含缓存操作、数据格式化、权限判断、消息发送等核心函数。
12.1 核心函数分类
// ============ 缓存相关 ============set_cache($key, $value, $date = 86400) // 设置缓存get_cache($key) // 读取缓存clear_cache($key) // 清空缓存
// ============ 配置相关 ============get_config($key) // 读取文件配置get_system_config($name, $key) // 读取数据库配置(带缓存)set_system_config($name, $key, $value) // 设置数据库配置
// ============ 用户相关 ============get_admin($uid) // 获取用户信息(带缓存)set_password($pwd, $salt) // 密码加密 (md5(pwd + salt))make_token() // 生成Token
// ============ 数据处理 ============get_params() // 获取请求参数(统一入口)to_assign($code, $msg, $data) // 统一JSON响应table_assign($code, $msg, $list) // 表格JSON响应to_date($time, $format) // 时间戳格式化
// ============ 权限相关 ============get_department_leader($uid, $level=0) // 获取部门负责人get_leader_departments($uid) // 获取用户负责的部门get_role_departments($uid) // 获取用户角色管理的部门isAuthExpense($uid) // 获取报销数据权限级别
// ============ 审批相关 ============check_status_name($status) // 审批状态名称get_check_status() // 审批状态列表
// ============ 日志相关 ============add_log($type, $id, $param, $subject) // 添加操作日志
// ============ 工具 ============is_mobile() // 判断移动端is_wxwork() // 判断企业微信get_codeno($type) // 生成单据编号12.2 关键函数详解
get_params() — 统一参数获取:
function get_params(){ // 合并 GET + POST 参数,应用 htmlspecialchars 过滤 return Request::param();}to_assign() — 统一 JSON 响应:
function to_assign($code = 0, $msg = '操作成功', $data = []){ $result = [ 'code' => $code, // 0=成功, 1=失败, 404=未登录, 405=无权限 'msg' => $msg, 'data' => $data ]; throw new HttpResponseException(json($result));}set_password() — 密码加密:
function set_password($pwd, $salt){ return md5(md5($pwd) . $salt);}// 双重 md5 + 盐值,虽然不是最优方案,但在 OA 系统中够用13. 推荐学习路线
13.1 按文件阅读顺序
第1步:理解配置 config/app.php → 应用配置 config/database.php → 数据库配置 composer.json → 依赖关系
第2步:理解认证体系 app/base/BaseController.php → ★ 核心基类(认证+权限拦截) app/home/controller/Login.php → 登录流程 extend/systematic/Systematic.php → RBAC权限引擎
第3步:理解CRUD范式 app/finance/controller/Expense.php → 标准控制器 app/finance/model/Expense.php → 标准模型 app/finance/validate/ExpenseValidate.php → 验证器
第4步:理解审批工作流(最复杂) app/api/controller/Check.php → ★ 审批引擎 app/api/controller/Comment.php → 评论系统
第5步:理解前端架构 app/base/view/common/base.html → 基础模板 app/finance/view/expense/datalist.html → 列表页模板 public/static/gougu/gouguInit.js → 前端入口
第6步:深入业务模块 app/contract/ → 合同管理 app/customer/ → 客户管理 app/project/ → 项目管理 app/user/ → 人事管理
第7步:工具函数 app/common.php → 1679行函数库(随时查阅)13.2 ThinkPHP 8 关键知识点对照
| ThinkPHP 8 特性 | 项目中的使用 |
|---|---|
| 多应用模式 | think-multi-app,18个子应用 |
| 控制器基类 | app\base\BaseController 抽象类 |
| 模型 | 继承 think\Model,五件套范式 |
| Db门面 | Db::name('Admin') 查询构造器 |
| Session门面 | Session::set/get/delete |
| Cache门面 | Cache::tag()->set/get 带标签缓存 |
| View门面 | View::assign() + 模板继承 |
| 验证器 | validate(ExpenseValidate::class)->scene('add')->check() |
| 事件系统 | event('SendMessage', $msg) |
| 中间件 | app/middleware.php 定义 |
| 路由 | 多应用自动路由,route/app.php 极简 |
13.3 Layui 关键知识点对照
| Layui 特性 | 项目中的使用 |
|---|---|
| 表格组件 | tablePlus(增强版),table.render() |
| 表单组件 | layui-form,lay-submit,lay-filter |
| 日期选择 | laydatePlus(增强版) |
| 弹窗 | layer.confirm(),layer.msg() |
| Tab切换 | layui.element.on('tab(tab)') |
| 工具集 | layui.tool.side(),layui.tool.delete() |
| 模板 | <script type="text/html"> + templet 函数 |
附录:数据库核心表结构
oa_admin 用户表(uid, username, pwd, salt, did, pid, position_id...)oa_department 部门表(树形结构,pid 父部门)oa_position 岗位表oa_admin_group 角色组(rules: 逗号分隔的规则ID)oa_position_group 岗位-角色组关联表oa_admin_rule 权限规则表(src: module/controller/action)oa_admin_log 操作日志
oa_flow_cate 审批流程分类(name, check_table, template_id...)oa_flow 审批流程定义(flow_list: 序列化步骤配置)oa_flow_step 审批步骤实例(action_id, sort, check_uids...)oa_flow_record 审批记录(check_uid, check_status, content...)
oa_expense 报销主表oa_expense_interfix 报销明细子表oa_expense_cate 报销分类oa_loan 借支表
oa_config 系统配置(content: 序列化配置数组)oa_file 文件表oa_message 消息表oa_message_template 消息模板文档总结:勾股OA 是一个结构清晰、范式统一的企业OA系统。其核心价值在于:
- BaseController 统一处理认证、权限、锁屏、超时
- 审批工作流引擎通过
check_name + action_id与业务完全解耦- Model 五件套范式让所有模块代码风格一致,易于理解和维护
- Layui + gouguInit 前端架构简洁高效,适合后台管理系统
建议按照第13章的学习路线逐文件阅读源码,配合实际运行调试,可以快速掌握 ThinkPHP 8 + Layui 的企业级开发方法。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!
樊笼










