勾股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 的企业级开发方法。
- 本文链接: https://richfan.eu.org/posts/%E7%A8%8B%E6%8A%80/php/%E5%8B%BE%E8%82%A1oa%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/
- 版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。
