勾股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. 目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| 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 核心依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| { "require": { "php": ">=8.2", "ext-curl": "*", "ext-fileinfo": "*", "ext-openssl": "*", "ext-pdo": "*", "ext-zip": "*", "topthink/framework": "^8.1.2", "topthink/think-orm": "^4.0.5", "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", "phpoffice/phpspreadsheet": "^1.2", "phpoffice/phpword": "^1.2", "mpdf/mpdf": "^8.1", "alibabacloud/dysmsapi-20170525": "^4.3" } }
|
PSR-4 自动加载
1 2 3 4 5 6
| { "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 第一段自动映射到应用目录:
1 2 3 4 5
| URL: /finance/expense/datalist ↓ ↓ ↓ 应用名 控制器 方法 ↓ ↓ ↓ app/finance Expense datalist()
|
对应代码文件:
1 2 3 4
| app/finance/controller/Expense.php → namespace app\finance\controller; → class Expense extends BaseController → public function datalist()
|
4.2 配置文件
config/app.php 核心配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| return [ 'app_host' => env('app.host', ''), 'app_namespace' => '', 'with_route' => true, 'default_app' => 'home', 'default_timezone' => 'Asia/Shanghai', 'default_filter' => 'htmlspecialchars', 'page_size' => 20, 'file_size' => 50, 'session_admin' => 'gougu_admin', '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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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_', 'charset' => 'utf8mb4', 'fields_strict' => true, ], ], ];
|
4.4 路由文件
route/app.php 极其简洁,几乎不定义路由,完全依赖多应用自动路由:
1 2 3 4 5 6 7
| use think\facade\Route;
Route::get('think', function () { return 'hello,ThinkPHP6!'; });
Route::get('hello/:name', 'index/hello');
|
4.5 入口文件
public/index.php(标准 ThinkPHP 8 入口):
1 2 3 4 5 6 7 8 9
| <?php
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 完整源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| 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; protected $pid; 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')); } }
|
5.2 认证流程(checkLogin)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| protected function checkLogin() { if ($this->controller !== 'login' && $this->controller !== 'captcha') { $session_admin = get_config('app.session_admin'); if (!Session::has($session_admin)) { if (request()->isAjax()) { return to_assign(404, '请先登录'); } else { redirect('/home/login/index.html')->send(); exit; } } else { $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']; $last_login_time = Db::name('Admin') ->where(['id' => $this->uid]) ->value('last_login_time'); $timeDiff = time() - $last_login_time; if ($timeDiff > 36000) { Session::delete($session_admin); redirect('/home/login/index.html')->send(); exit; } Db::name('Admin') ->where(['id' => $this->uid]) ->update(['last_login_time' => time()]); if ($is_lock == 1) { redirect('/home/login/lock.html')->send(); exit; } View::assign('login_admin', $login_admin); $not_check = ['index','leaves','outs','overtimes','trips','message']; if ($this->module == 'home' && in_array($this->controller, $not_check)) { return true; } else { $regPwd = $login_admin['reg_pwd']; if ($regPwd !== '') { redirect('/home/index/edit_password.html')->send(); exit; } if (!$this->checkAuth()) { if (request()->isAjax()) { return to_assign(405, '你没有权限'); } else { redirect('/home/index/role')->send(); exit; } } } } } }
|
5.3 权限检查(checkAuth)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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 认证流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 请求进入 │ ├─ 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| abstract class BaseController { protected $jwt_conf = [ 'secrect' => 'gouguoa', 'iss' => 'www.gougucms.com', 'aud' => 'gouguoa', 'exptime' => 7200, ]; 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 登录页展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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(); } }
|
注意:Login 类不继承 BaseController,因为登录页不需要认证。这是唯一不继承 BaseController 的控制器。
6.2 登录提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| public function login_submit() { $param = get_params(); try { validate(UserCheck::class)->check($param); } catch (ValidateException $e) { return to_assign(1, $e->getError()); }
$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, '用户名或手机号码错误'); } } $param['pwd'] = set_password($param['password'], $admin['salt']); if ($admin['pwd'] !== $param['pwd']) { return to_assign(1, '用户或密码错误'); } if ($admin['status'] != 1) { return to_assign(1, '该用户禁止登录'); } $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); $session_admin = get_config('app.session_admin'); Session::set($session_admin, $admin['id']); $token = make_token(); set_cache($token, $admin, 7200); $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 锁屏与解锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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']]); } Db::name('admin')->where('id', $admin['id']) ->update(['is_lock' => 1]); return View(); }
|
6.4 登录流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 用户输入用户名/密码 │ ▼ 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 采用 岗位 → 角色组 → 权限规则 三级模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 用户(Admin) │ ├── position_id → 岗位(Position) │ │ │ └── PositionGroup → 角色组(AdminGroup) │ │ │ └── rules (逗号分隔的规则ID) │ │ │ └── AdminRule │ ├── src: "finance/expense/datalist" │ ├── title: "报销列表" │ └── ... │ ├── did → 部门(Department) └── pid → 岗位ID
|
7.2 核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| 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')) { $position_id = Db::name('Admin') ->where('id', $uid) ->value('position_id'); $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(); $ids = []; foreach ($groups as $g) { $ids = array_merge($ids, explode(',', trim($g['rules'], ','))); } $ids = array_unique($ids); $rules_all = Db::name('AdminRule') ->field('src')->select()->toArray(); $rules = Db::name('AdminRule') ->where('id', 'in', $ids) ->field('src')->select()->toArray(); $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']); } Cache::tag('adminRules')->set('RulesSrc0', $auth_list_all, 36000); Cache::tag('adminRules')->set('RulesSrc' . $uid, $auth_list, 36000); } } }
|
7.3 权限匹配机制
在 BaseController::checkAuth() 中:
1 2 3 4 5 6 7
| $pathUrl = $this->module . '/' . $this->controller . '/' . $this->action;
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 数据模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| 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']; 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()]); $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) { 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; } 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++; } } Db::name('FlowStep')->insertAll($step); 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'] ?? '' ]); 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| 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) { $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); 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; } } $param['check_history_uids'] = empty($detail['check_history_uids']) ? $this->uid : $detail['check_history_uids'] . ',' . $this->uid; Db::name($check_table)->where('id', $action_id)->update($param); 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(), ]); 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', [...]); } } } }
|
8.5 审批状态流转
1 2 3 4 5 6 7 8 9 10 11 12 13
| ┌──────────────────────────────────────────────┐ │ │ ▼ │ 0(待提交) ──提交──→ 1(审批中) ──通过──→ 2(审核完成) │ │ │ │ │ ├──拒绝──→ 3(审核拒绝) │ │ │ │ │ │ │ └─check_role=5──→ │ │ │ 回退上一步 │ │ │ │ │ └──撤回──→ 4(撤回) ──反确认──→ 0(待提交) │ └──反确认──────────────────────────────────────→ 0(待提交)
|
8.6 工作流与业务表的关系
审批工作流通过 check_name + action_id 与业务表完全解耦:
1 2 3 4 5 6 7 8 9 10 11
| 业务表 (如 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 控制器结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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数据过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| 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(); } }
|
9.3 add — 添加/编辑合一 + 借支冲抵
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| 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'); $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 五件套范式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
| namespace app\finance\model;
use think\model; use think\facade\Db;
class Expense extends Model { 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()]; } } 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]); } 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']]); } 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; } 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 实现方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| $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]; } }
|
10.3 FIND_IN_SET 的使用
FIND_IN_SET 是 MySQL 内置函数,用于在逗号分隔的字符串中查找值:
1 2
| SELECT * FROM oa_expense WHERE FIND_IN_SET('5', check_uids);
|
在 ThinkPHP 8 中使用 Db::raw() 表达式:
1
| $where[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_uids)")];
|
11. Layui 前端架构
11.1 基础模板继承
文件:app/base/view/common/base.html
所有页面通过 ThinkPHP 模板继承复用基础结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <!DOCTYPE html> <html lang="zh"> <head> <meta charset="utf-8" /> <title>{:get_system_config('web','admin_title')}</title> {block name="css"} <link rel="stylesheet" href="{__GOUGU__}/gougu/css/gougu.css?v={:get_system_config('system','version')}"> {/block} {block name="style"}{/block} <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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| {extend name="../../base/view/common/base" /}
{block name="body"} <div class="p-page"> <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']; function gouguInit() { var table = layui.tablePlus, tool = layui.tool, laydatePlus = layui.laydatePlus; 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 前端交互模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 页面加载 │ ▼ 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 核心函数分类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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) make_token()
get_params() to_assign($code, $msg, $data) table_assign($code, $msg, $list) 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() — 统一参数获取:
1 2 3 4 5
| function get_params() { return Request::param(); }
|
to_assign() — 统一 JSON 响应:
1 2 3 4 5 6 7 8 9
| function to_assign($code = 0, $msg = '操作成功', $data = []) { $result = [ 'code' => $code, 'msg' => $msg, 'data' => $data ]; throw new HttpResponseException(json($result)); }
|
set_password() — 密码加密:
1 2 3 4 5
| function set_password($pwd, $salt) { return md5(md5($pwd) . $salt); }
|
13. 推荐学习路线
13.1 按文件阅读顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 第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 函数 |
附录:数据库核心表结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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 的企业级开发方法。