<?php
/**
 * ===========================================================================
 * Veitool 快捷开发框架系统
 * Author: Niaho 26843818@qq.com
 * Copyright (c)2019-2025 www.veitool.com All rights reserved.
 * Licensed: 这不是一个自由软件，不允许对程序代码以任何形式任何目的的再发行
 * ---------------------------------------------------------------------------
 */
declare (strict_types=1);

namespace app;

use app\event\PhpOffice;
use think\App;
use think\facade\Cache;
use think\facade\Db;
use think\Response;
use think\facade\View;
use think\exception\HttpResponseException;

/**
 * 控制器抽象基类
 */
abstract class BaseController
{
    /**
     * 应用实例
     * @var App
     */
    protected $app;

    /**
     * Request实例
     * @var \think\Request
     */
    protected $request;

    /**
     * Token
     * @var string
     */
    protected $token = '';

    /**
     * Token 名
     * @var string
     */
    protected $tokenName = '__token__';

    /**
     * 信息模板
     * @var string
     */
    protected $msgTpl = '';

    /**
     * 控制器中间件
     * @var array
     */
    protected $middleware = [];

    /**
     * 前台会员信息
     * @var array
     */
    protected $memUser = [];

    /**
     * 构造方法
     * @param App $app
     */
    public function __construct(App $app)
    {
        // 应用实例
        $this->app = $app;
        // 请求对象
        $this->request = $this->app->request;
        // 前台集中业务 获取应用名 app('http')->getName() 获取IP $this->request->ip()
        $this->__home();
        // 验证（登录、权限）
        $this->__auth();
        // 控制器初始化
        $this->__init();
        $this->logon();
        //访问量
        $this->__website();
    }

    protected function __website()
    {

        $ip = $this->request->ip();
        $websitelog = Cache::get('webistelog:' . $ip);
        if ($websitelog) {
            return;
        }

        $filename = 'data_lock_' . $ip . '.txt';
        $fp = fopen($filename, 'c + '); // 'c + ' 模式避免截断文件
        try {
            // 尝试获取锁
            if (flock($fp, LOCK_EX)) {

                $websitelog = Cache::get('webistelog:' . $ip);
                if ($websitelog) {
                    return;
                }
                $url = substr(vhtmlspecialchars(strip_sql($this->request->url())), 0, 200);
                $a = substr(vhtmlspecialchars(strip_sql(request()->header('user-agent'))), 0, 200);
                $data['ip'] = $ip;
                $data['url'] = $url;
                $data['user_agent'] = $a;
                $data['logtime'] = time();
                Db::name('webiste_log')->insert($data);
                Db::name('webiste')->where('id', 1)->setInc('unique_visitors');
                Cache::set('webistelog:' . $ip, $ip, 72000);
                // 释放锁
                flock($fp, LOCK_UN);
            }
            fclose($fp);
            unlink($filename);
        } catch (\Exception $e) {
            // 确保发生异常时释放锁并关闭文件
            if (is_resource($fp)) {
                flock($fp, LOCK_UN);
                fclose($fp);
                unlink($filename);
            }
        }


    }

    /**
     * 前台集中业务
     */
    protected function __home()
    {
        // 前台统一开关 需后台配置参数 开关类型:site_close 和 文本域类型:site_close_tip
        if (vconfig('site_close')) $this->exitMsg(vconfig('site_close_tip', '系统升级维护中，请稍后访问！'), 400);
        // 获取会员信息
        $this->memUser = session(VT_MEMBER);
    }

    /**
     * 验证（登录、权限）
     */
    protected function __auth()
    {
    }

    /**
     * 初始化
     */
    protected function __init()
    {
    }

    /**
     * 空方法
     */
    public function __call($name, $arg)
    {
        $this->exitMsg('Method does not exist', 400);
    }

    /**
     * 日志/在线处理
     * @access  protected
     * @param string $tip 提示
     */
    protected function logon(string $tip = '')
    {
        $flag1 = vconfig('home_log', 0);

        $flag2 = in_array(vconfig('online_on', 0), [2, 3]);
        if ($flag1 || $flag2) $url = substr(vhtmlspecialchars(strip_sql($this->request->url())), 0, 200);
        /*访问日志*/
        if ($flag1) {
            \app\model\system\SystemWebLog::add(['url' => $url . $tip, 'username' => $this->memUser['username'] ?? '', 'ip' => $this->request->ip()]);
        }/**/
        /*在线统计【0:关闭全部 1:开启后台 2:开启会员 3:开启全部】*/
        if ($flag2) {
            \app\model\system\SystemOnline::recod($this->memUser, $url);
        }/**/
    }

    /**
     * 模板赋值
     * @access  protected
     * @param string|array $vars 赋值表达式/数组
     */
    protected final function assign(...$vars)
    {
        View::assign(...$vars);
    }

    /**
     * 模板渲染
     * @access  protected
     * @param string $tmp 模板名称
     * @param string $tip 提示
     * @param bool $logon 记录日志
     */
    protected final function fetch(string $tmp = '', string $tip = '', bool $logon = true)
    {
        $logon && $this->logon($tip);
        return View::fetch($tmp);
    }

    /**
     * 重定向
     * @access  protected
     * @param string $args 重定向地址
     * @throws  HttpResponseException
     */
    protected final function redirect(...$args)
    {
        throw new HttpResponseException(redirect(...$args));
    }

    /**
     * 中断反馈信息
     * @access  protected
     * @param string $m 信息字符
     * @param int $c 状态值 400前台关闭 401 Ajax请求未登陆 303网址请求未登录
     * @param array $d 数组信息
     * @param array $h 发送的Header信息
     * @throws  HttpResponseException
     */
    protected final function exitMsg($m, $c = 0, $d = [], $h = [])
    {
        if ($c == 400) {
            $re = Response::create(ROOT_PATH . 'app / v_msg . tpl', 'view')->assign(['msg' => $m, 'site' => vconfig('site_title')])->header($h);
        } else if ($c == 303) {
            $re = Response::create(ROOT_PATH . 'app / v_msg . tpl', 'view')->assign(['msg' => $m, 'site' => vconfig('site_title'), 'url' => $d['url']])->header($h);
        } else {
            $rs = json_encode(['code' => $c, 'msg' => $m, 'data' => $d, 'token' => $this->token]);
            $re = Response::create($rs)->header($h);
        }
        throw new HttpResponseException($re);
    }

    /**
     * 返回组信息
     * @access  protected
     * @param string|array|obj $msg 信息字符
     * @param int $code 状态码
     * @param array $data 数组信息
     * @param int $scode 页头状态码
     * @param array $header 头部
     * @param array $options 参数
     * @return  html/json
     */
    protected final function returnMsg($msg = '', $code = 0, $data = [], $scode = 200, $header = [], $options = [])
    {
        $msg = is_object($msg) ? $msg->toArray() : $msg;
        if (is_array($msg)) {
            if (isset($msg['total'])) { //分页模式
                $data = $msg['data'];
                $count = $msg['total'];
                $data['msg'] = $msg['msg'] ?? '';
            } else {
                $data = $msg;
            }

            $msg = $data['msg'] ?? '';
            $code = $data['code'] ?? $code;
            unset($data['msg'], $data['code']);
            $data = $data['data'] ?? $data;
        } elseif ($this->msgTpl === '') {
            $this->logon((string)$msg);
        }
        $token = $this->token;
        $count = isset($count) ? $count : (is_array($data) ? count($data) : 1);
        if ($this->msgTpl) {
            $this->assign(compact('code', 'msg', 'data', 'count', 'token'));
            return $this->fetch($this->msgTpl);
        } else {
            return json(compact('code', 'msg', 'data', 'count', 'token'), $scode, $header, $options);
        }
    }

    /**
     * 带模板反馈提示
     * @access   protected
     * @param string $msg 提示信息
     * @param int $tpl 提示模板
     * @param string $url 跳转的地址
     */
    protected final function returnTpl($msg = '', $tpl = '', $url = '')
    {
        $tpl = $tpl ?: ($this->request->isMobile() ? 'err' : ROOT_PATH . 'app / v_msg . tpl');
        $this->assign(['msg' => $msg, 'url' => $url]);
        return $this->fetch($tpl);
    }

    /**
     * 获取指定的参数 过滤方法后执行【key / *或?表非空时验证或$表非空时验证不规范则置空不中断 / 规则(e邮箱m手机c身份证p密码u帐号n姓名i数串a插件名v配置名)或位数范围如{1,3} / 提示(传入优先) / 合法的字符集0,1..串 默认0:字母数字汉字下划线 1:数字 2:小写字母 3:大写字母 4:汉字 5:任何非空白字符 / 允许的字符】
     * @access protected
     * @param array $name 变量名 /a转数组 /d整数 /f浮点 /b布尔 /s字符串 /u网址净化 /h全净化去标签 /c转为HTML实体 /r转为2位小数 /*验证【默认允许：汉字|字母|数字|下划线|空格.#-】
     * @param mixed $type 方法类型 默认 post
     * @param string|array $filter 过滤方法 默认 strip_sql
     * @param bool $bin 是否以传入数组为准 默认是
     * @return array
     */
    protected final function only($name = [], $type = 'post', $filter = 'strip_sql', $bin = true)
    {
        if (isset($name['@token'])) {
            if (!env('APP_DEBUG')) {
                $arr = array_merge([$this->tokenName, []], (array)$name['@token']);
                $this->request->checkToken($arr[0], $arr[1]) === false && $this->exitMsg("Token错误");
                $this->token = token($this->tokenName);
            }
            unset($name['@token']);
            if (!$name) return [];
        }
        $item = [];
        $data = $this->request->$type(false);
        $preg = [
            'e' => [2 => 'email', 3 => '邮箱地址格式错误', 4 => '', 5 => ''],
            'm' => [2 => 'mobile', 3 => '手机号码格式错误', 4 => '', 5 => ''],
            'c' => [2 => 'idcard', 3 => '身份证号格式错误', 4 => '', 5 => ''],
            'p' => [2 => '{
                6,16}', 3 => '密码', 4 => '5', 5 => ''],
            'u' => [2 => '{
                4,30}', 3 => '帐号', 4 => '1,2,3', 5 => ' . _@'],
            'n' => [2 => '{
                2,30}', 3 => '姓名', 4 => '0', 5 => ' . '],
            'i' => [2 => '{
                1,30}', 3 => '数串', 4 => '1', 5 => ','],
            'a' => [2 => '{
                3,20}', 3 => '插件名', 4 => '1,2', 5 => ''],
            'v' => [2 => '{
                2,20}', 3 => '配置名', 4 => '1,2,3', 5 => '_']
        ];
        foreach ($name as $key => $val) {
            $default = '';
            $sub = ['', '', '', '', '0', ' .#-']; // 对应['key','转换类型|验证符*或?','验证规则','提示','合法的字符集','允许的字符']
            if (strpos($val, '/')) {
                $sub = explode('/', $val) + $sub;
                $val = $sub[0];
            }
            $flag = true; //用于是否 $filter 过滤控制
            if (is_int($key)) {
                $key = $val;
                if ($key[0] == '@') {
                    $flag = false;
                    $key = ltrim($key, '@');
                }
                if (!key_exists($key, $data) && !$sub[1]) {
                    $item[$key] = $default;
                    continue;
                }
            } else {
                $default = $val;
                if ($key[0] == '@') {
                    $flag = false;
                    $key = ltrim($key, '@');
                }
            }
            $v = $data[$key] ?? $default;
            if ($sub[1]) {
                $must = $msg = true; // $must:是否必须验证  $msg:是否验证不规范时中断反馈提示
                if (in_array($sub[1], ['?', '$'])) {
                    $must = $v ? true : false;
                    if ($sub[1] == '$') $msg = false;
                    $sub[1] = '*';
                }
                switch ($sub[1]) {
                    case 'a':
                        $v = $v ? (array)$v : [];
                        break;
                    case 'd':
                        $v = (int)$v;
                        break;
                    case 'u':
                        $v = strip_html($v, 0);
                        break;
                    case 'h':
                        $v = strip_html($v);
                        break;
                    case 'c':
                        $v = vhtmlspecialchars($v);
                        break;
                    case 'r':
                        $v = dround($v);
                        break;
                    case '*':

                        if ($sub[2] == 'p') $must = is_md5($v) ? false : $must;
                        $tip = $sub[3];
                        if (isset($preg[$sub[2]])) {
                            $sub = $preg[$sub[2]] + $sub;
                            $tip = $tip ?: $sub[3];
                        }
                        $reg = explode(',', $sub[4]);
                        if ($must && !is_preg($v, $sub[2], $reg, $sub[5])) {
                            if ($msg) {
                                $tip = $tip ?: "字段{$key}不合规范";
                                $txt = ['汉字字母数字下划线', '数字', '小写字母', '大写字母', '汉字', '任何非空白字符'];
                                if ($reg[0] !== '') {
                                    $str = '';
                                    foreach ($reg as $i) {
                                        $str .= ($txt[$i] ?? '') . '、';
                                    }
                                    $tip = $tip . '必须由' . (str_replace(['{', ',', '}'], ['', '-', ''], $sub[2])) . '位' . rtrim($str, '、') . ($sub[5] ? '和' . str_replace(' ', '空格', $sub[5]) : '') . '组成';
                                }
                                $this->exitMsg($tip);
                            } else {
                                $v = '';
                            }
                        }
                        break;
                    case 'f':
                        $v = (float)$v;
                        break;
                    case 'b':
                        $v = (boolean)$v;
                        break;
                    case 's':
                        if (is_scalar($v)) {
                            $v = (string)$v;
                        } else {
                            throw new \InvalidArgumentException('variable type error：' . gettype($v));
                        }
                        break;
                }
                if ($sub[1] != '*' && $sub[2] && !$v) $this->exitMsg($sub[2]);
            }
            $item[$key] = $flag ? call_user_func($filter, $v) : $v;
        }
        return $bin ? $item : $item + $data;
    }


    public function downloadfile($fileid, $excel = 0)
    {
        if ($excel == 1) {
            //导出下载
            $fileinfo = Db::name('excel_down')->where(['id' => $fileid])->find();
            $fileurl = strstr($fileinfo['fileurl'], '/static');
            return download('.' . $fileurl, $fileinfo['filename'])->force(true);
        } else {
            $fileinfo = get_upload_file($fileid, 'info');
            if ($fileinfo['storage'] == 'local') {
                //本地文件下载
                $fileurl = strstr($fileinfo['fileurl'], '/static');
                return download('.' . $fileurl, $fileinfo['filename'])->force(true);
            } else {
                try {
                    //oss文件下载 先保存到服务器 然后再下载到本地电脑
                    $path = 'static/tempdown/' . $fileinfo['filename'];
                    PhpOffice::ossdownfile($fileinfo['fileurl'], $path);
                    register_shutdown_function(function () use ($path) {
                        // 处理完成后删除临时文件
                        unlink(VT_PUBLIC . '/' . $path);
                    });
                    return download('./' . $path, $fileinfo['filename'])->force(true);
                } catch (\Exception $e) {
                    return $this->exitMsg($e->getMessage());
                }
            }
        }
    }

}