,"message":"...","data":null} * 2) Authorization: Bearer {token} 鉴权(前端约定,FastAdmin 原生只认 HTTP_TOKEN) * 3) 分页结构:{list,page,size,total,hasMore} * 4) 业务配置读写(fa_fc_config) * * 契约见 docs/03-接口文档.md */ class V1 extends Api { /** 全部方法自行鉴权,不走 FastAdmin 的 uri 权限匹配 */ protected $noNeedLogin = ['*']; protected $noNeedRight = ['*']; /** @var \app\common\model\User|null 当前登录用户 */ protected $user = null; /** @var bool 是否已尝试解析登录态 */ protected $resolved = false; /* ==================== 响应输出 ==================== */ /** * 成功响应 * @param mixed $data * @param string $message */ protected function ok($data = null, $message = 'ok') { $this->emit(['code' => 0, 'message' => $message, 'data' => $data], 200); } /** * 业务失败响应(HTTP 默认 200,前端按 code 判定) * @param string $message * @param int $code * @param int $httpStatus */ protected function fail($message, $code = 1, $httpStatus = 200) { $this->emit(['code' => $code, 'message' => (string)$message, 'data' => null], $httpStatus); } /** 输出 JSON 并终止 */ protected function emit(array $payload, $httpStatus = 200) { $response = Response::create($payload, 'json', $httpStatus) ->header(['Content-Type' => 'application/json;charset=utf-8']); throw new HttpResponseException($response); } /* ==================== 鉴权 ==================== */ /** 从 Authorization: Bearer xxx 提取 token */ protected function bearerToken() { $auth = $this->request->header('authorization', ''); if ($auth && stripos($auth, 'bearer ') === 0) { return trim(substr($auth, 7)); } return ''; } /** 解析当前用户(不强制登录) */ protected function user() { if ($this->resolved) { return $this->user; } $this->resolved = true; $token = $this->bearerToken(); if (!$token && !($token = $this->request->request('token'))) { return $this->user = null; } $row = Db::name('user_token')->where('token', $token)->find(); if (!$row) { return $this->user = null; } if ($row['expiretime'] && $row['expiretime'] < time()) { return $this->user = null; } $user = \app\common\model\User::get($row['user_id']); if (!$user || $user->status != 'normal') { return $this->user = null; } return $this->user = $user; } /** 强制登录:未登录直接 401 */ protected function auth() { $user = $this->user(); if (!$user) { $this->fail('登录已过期,请重新登录', 401, 401); } return $user; } /** 当前用户 ID */ protected function userId() { return (int)$this->auth()->id; } /** 签发 token 并写入 fa_user_token */ protected function issueToken($userId, $days = 30) { $token = md5(mt_rand() . $userId . microtime(true) . uniqid('', true)); Db::name('user_token')->insert([ 'token' => $token, 'user_id' => $userId, 'createtime' => time(), 'expiretime' => time() + $days * 86400, ]); return $token; } /* ==================== 入参 ==================== */ /** 读取单个入参(POST 优先,其次 GET) */ protected function input($key, $default = null) { $val = $this->request->post($key, null); if ($val === null || $val === '') { $val = $this->request->get($key, $default); } return $val === null ? $default : $val; } /** 全部入参 */ protected function inputs() { return array_merge((array)$this->request->get(), (array)$this->request->post()); } /** 必填校验,失败直接返回业务错误 */ protected function require2($fields, $data = null) { $data = $data === null ? $this->inputs() : $data; foreach ((array)$fields as $f => $label) { if (is_int($f)) { $f = $label; $label = $f; } if (!isset($data[$f]) || $data[$f] === '' || $data[$f] === null) { $this->fail($label . '不能为空'); } } return true; } /* ==================== 分页 ==================== */ /** 解析分页参数 -> [page, size] */ protected function pager($defaultSize = 10, $maxSize = 50) { $page = max(1, (int)$this->input('page', 1)); $size = (int)$this->input('size', $defaultSize); if ($size < 1) { $size = $defaultSize; } if ($size > $maxSize) { $size = $maxSize; } return [$page, $size]; } /** 组装分页结构 {list,page,size,total,hasMore} */ protected function paginate($list, $total, $page, $size) { if (is_object($list) && method_exists($list, 'all')) { $list = $list->all(); } $list = array_values(is_array($list) ? $list : (array)$list); return [ 'list' => $list, 'page' => (int)$page, 'size' => (int)$size, 'total' => (int)$total, 'hasMore' => $page * $size < $total, ]; } /** 对数组集合做内存分页(小数据集用) */ protected function paginateArray(array $items, $page, $size) { $total = count($items); return $this->paginate(array_slice($items, ($page - 1) * $size, $size), $total, $page, $size); } /* ==================== 业务配置(fa_fc_config) ==================== */ /** * 读取配置,key 形如 fee.singlePost / ui.logo / base.appName * 数组/对象值以 JSON 存取 */ protected function cfg($key, $default = null) { static $cache = null; if ($cache === null) { $cache = []; foreach (Db::name('fc_config')->select() as $row) { $cache[$row['key']] = $row['value']; } } if (!array_key_exists($key, $cache)) { return $default; } $val = $cache[$key]; $decoded = json_decode($val, true); if (json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || $decoded === null)) { return $decoded; } return $val; } /** 写入配置 */ protected function setCfg($key, $value) { $val = (is_scalar($value) || $value === null) ? (string)$value : json_encode($value, JSON_UNESCAPED_UNICODE); if (Db::name('fc_config')->where('key', $key)->find()) { Db::name('fc_config')->where('key', $key)->update(['value' => $val, 'updatetime' => time()]); } else { Db::name('fc_config')->insert([ 'key' => $key, 'value' => $val, 'createtime' => time(), 'updatetime' => time(), ]); } return true; } /* ==================== 工具 ==================== */ /** 时间戳 -> ISO8601(前端 new Date() 可直接解析) */ protected function iso($timestamp) { if (!$timestamp) { return null; } if (!is_numeric($timestamp)) { $timestamp = strtotime($timestamp); } return date('c', (int)$timestamp); } /** 手机号脱敏 */ protected function maskPhone($phone) { $phone = (string)$phone; return strlen($phone) === 11 ? substr($phone, 0, 3) . '****' . substr($phone, 7) : $phone; } /** 生成业务单号 */ protected function orderNo($prefix) { return $prefix . date('YmdHis') . mt_rand(1000, 9999); } /* ==================== 业务公共 ==================== */ /** * 读取用户业务扩展(不存在则初始化) * @return array */ protected function ext($userId) { $ext = Db::name('fc_user_ext')->where('user_id', $userId)->find(); if (!$ext) { $inviteCode = strtoupper(substr(md5('fc' . $userId . mt_rand()), 0, 6)); Db::name('fc_user_ext')->insert([ 'user_id' => $userId, 'openid' => '', 'user_type' => 1, 'realname_status' => 0, 'is_member' => 0, 'member_level' => '', 'member_expiretime' => null, 'invite_code' => $inviteCode, 'inviter_id' => 0, 'free_view_date' => date('Y-m-d'), 'free_view_used' => 0, 'createtime' => time(), 'updatetime' => time(), ]); $ext = Db::name('fc_user_ext')->where('user_id', $userId)->find(); } return $ext; } /** * 组装用户信息(与前端 common/mock/data.js 的 datasets.user 字段一致) */ protected function userInfo($user = null, $phone = null) { $user = $user ?: $this->auth(); $ext = $this->ext($user['id']); $isMember = (int)$ext['is_member'] === 1 && (!$ext['member_expiretime'] || $ext['member_expiretime'] > time()); return [ 'id' => (int)$user['id'], 'nickname' => (string)$user['nickname'], 'avatar' => (string)$user['avatar'], 'phone' => $phone === null ? (string)$user['mobile'] : $phone, 'userType' => (int)$ext['user_type'], 'isMember' => $isMember, 'memberLevel' => $isMember ? (string)$ext['member_level'] : '', 'memberExpireAt' => $ext['member_expiretime'] ? $this->iso($ext['member_expiretime']) : null, 'balance' => (float)$user['money'], 'realnameStatus' => (int)$ext['realname_status'], ]; } /** 解析 JSON 字段为数组 */ protected function jsonArr($val) { if (is_array($val)) { return $val; } if (!$val) { return []; } $decoded = json_decode($val, true); return is_array($decoded) ? $decoded : []; } /** 记一条资金流水 */ protected function flow($type, $scene, $amount, $balance, $relatedId = 0, $remark = '') { static $seq = 0; $seq++; Db::name('fc_finance_flow')->insert([ 'flow_no' => 'F' . date('YmdHis') . mt_rand(100, 999) . $seq, 'type' => $type, 'scene' => $scene, 'amount' => $amount, 'balance' => $balance, 'related_id' => $relatedId, 'remark' => $remark, 'createtime' => time(), ]); } /** 发一条站内消息 */ protected function notify($userId, $type, $title, $content) { Db::name('fc_message')->insert([ 'user_id' => $userId, 'type' => $type, 'title' => $title, 'content' => $content, 'isread' => 0, 'readtime' => null, 'createtime' => time(), ]); } /** 管理端操作留痕 */ protected function adminLog($action, $remark = '') { try { Db::name('admin_log')->insert([ 'admin_id' => 1, 'username' => 'admin', 'url' => $this->request->url(), 'title' => $action, 'content' => $remark, 'ip' => $this->request->ip(), 'useragent' => (string)$this->request->header('user-agent'), 'createtime' => time(), ]); } catch (\Exception $e) { // 留痕失败不影响主流程 } } }