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

勾股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. 目录结构

text
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 核心依赖

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 自动加载

json
{
    "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 第一段自动映射到应用目录:

text
URL: /finance/expense/datalist
      ↓        ↓         ↓
   应用名    控制器      方法
      ↓        ↓         ↓
  app/finance  Expense  datalist()

对应代码文件:

text
app/finance/controller/Expense.php
  → namespace app\finance\controller;
  → class Expense extends BaseController
  → public function datalist()

4.2 配置文件

config/app.php 核心配置:

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

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 极其简洁,几乎不定义路由,完全依赖多应用自动路由:

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
<?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 完整源码

php
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)

php
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)

php
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 认证流程图

text
请求进入
  ├─ 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
php
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 登录页展示

php
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 登录提交

php
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 锁屏与解锁

php
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 登录流程图

text
用户输入用户名/密码
   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 采用 岗位 → 角色组 → 权限规则 三级模型:

text
用户(Admin)
  ├── position_id → 岗位(Position)
  │                    │
  │                    └── PositionGroup → 角色组(AdminGroup)
  │                                          │
  │                                          └── rules (逗号分隔的规则ID)
  │                                                    │
  │                                                    └── AdminRule
  │                                                         ├── src: "finance/expense/datalist"
  │                                                         ├── title: "报销列表"
  │                                                         └── ...
  ├── did → 部门(Department)
  └── pid → 岗位ID

7.2 核心代码

php
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() 中:

php
$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 数据模型

text
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)

php
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)

php
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 审批状态流转

text
                    ┌──────────────────────────────────────────────┐
                    │                                              │
                    ▼                                              │
  0(待提交) ──提交──→ 1(审批中) ──通过──→ 2(审核完成)               │
                    │              │                                │
                    │              ├──拒绝──→ 3(审核拒绝)            │
                    │              │              │                 │
                    │              │              └─check_role=5──→ │
                    │              │                  回退上一步    │
                    │              │                                 │
                    │              └──撤回──→ 4(撤回) ──反确认──→ 0(待提交)
                    └──反确认──────────────────────────────────────→ 0(待提交)

8.6 工作流与业务表的关系

审批工作流通过 check_name + action_id 与业务表完全解耦

text
业务表 (如 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 控制器结构

php
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数据过滤

php
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 — 添加/编辑合一 + 借支冲抵

php
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 五件套范式:

php
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 实现方式

php
// 在 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 内置函数,用于在逗号分隔的字符串中查找值:

sql
-- 查找 check_uids 字段中包含 uid=5 的记录
SELECT * FROM oa_expense WHERE FIND_IN_SET('5', check_uids);

在 ThinkPHP 8 中使用 Db::raw() 表达式:

php
$where[] = ['', 'exp', Db::raw("FIND_IN_SET('{$uid}',check_uids)")];

11. Layui 前端架构

11.1 基础模板继承

文件:app/base/view/common/base.html

所有页面通过 ThinkPHP 模板继承复用基础结构:

html
<!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

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 前端交互模式

text
页面加载
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 核心函数分类

php
// ============ 缓存相关 ============
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() — 统一参数获取:

php
function get_params()
{
    // 合并 GET + POST 参数,应用 htmlspecialchars 过滤
    return Request::param();
}

to_assign() — 统一 JSON 响应:

php
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() — 密码加密:

php
function set_password($pwd, $salt)
{
    return md5(md5($pwd) . $salt);
}
// 双重 md5 + 盐值,虽然不是最优方案,但在 OA 系统中够用

13. 推荐学习路线

13.1 按文件阅读顺序

text
第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 函数

附录:数据库核心表结构

text
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 的企业级开发方法。