勾股OA 源码解读 — ThinkPHP 8 + Layui 企业OA系统

8131 字
41 分钟
勾股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+


目录#

  1. 项目总览
  2. 目录结构
  3. 技术栈与依赖
  4. ThinkPHP 8 多应用架构
  5. BaseController — 认证与权限拦截
  6. 登录流程精读
  7. RBAC 权限引擎 — Systematic.php
  8. 审批工作流 — Check.php
  9. 财务模块 — 标准 CRUD 范式
  10. 数据权限设计
  11. Layui 前端架构
  12. 工具函数库 — common.php
  13. 推荐学习路线

1. 项目总览#

勾股OA(GouGuOA)是一套基于 ThinkPHP 8 + Layui 构建的企业级办公自动化系统,涵盖行政、财务、合同、客户、项目等完整业务闭环。项目采用多应用架构,共 18 个子应用80+ 控制器70+ 模型,代码量大但结构清晰,是学习 ThinkPHP 8 实战开发的优秀范例。

核心业务模块#

应用路径功能说明
homeapp/home/首页、登录、权限规则、消息、审批中心、请假/外出/加班/出差
userapp/user/员工管理、部门、岗位、劳动合同、关怀、奖惩
financeapp/finance/报销、借支、开票、收款、付款、票据
contractapp/contract/合同管理、采购、供应商、产品
customerapp/customer/客户管理、联系人、商机、跟进记录
projectapp/project/项目管理、任务、文档、里程碑
admapp/adm/行政管理:印章、车辆、会议、公文、资产、公告
oaapp/oa/日常办公:日程、计划、工作汇报、会议纪要
apiapp/api/API接口:审批流程、评论、导入导出
baseapp/base/基础控制器和视图模板(不直接对外)
qiyeapp/qiye/企业微信端
mobileapp/mobile/移动端菜单配置
diskapp/disk/网盘文件管理
crudapp/crud/代码生成器
installapp/install/安装向导
listenerapp/listener/事件监听器
exceptionapp/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 → 岗位ID

7.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-formlay-submitlay-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系统。其核心价值在于:

  1. BaseController 统一处理认证、权限、锁屏、超时
  2. 审批工作流引擎通过 check_name + action_id 与业务完全解耦
  3. Model 五件套范式让所有模块代码风格一致,易于理解和维护
  4. Layui + gouguInit 前端架构简洁高效,适合后台管理系统

建议按照第13章的学习路线逐文件阅读源码,配合实际运行调试,可以快速掌握 ThinkPHP 8 + Layui 的企业级开发方法。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
勾股OA 源码解读 — ThinkPHP 8 + Layui 企业OA系统
https://fanrich.eu.org/posts/程技/php/勾股oa源码解读/
作者
richfan
发布于
2026-06-20
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
richfan
Hello, I'm richfan.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
分类
标签
站点统计
文章
779
分类
8
标签
138
总字数
3,267,679
运行时长
0
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.13.5
文章许可
CC BY-NC-SA 4.0

文章目录