存客宝应用接口初始化

This commit is contained in:
Ghost
2026-01-05 10:25:32 +08:00
parent fa37227954
commit 02797ba50a
414 changed files with 85530 additions and 75 deletions

View File

@@ -0,0 +1,151 @@
<?php
namespace app\common\controller;
use library\ResponseHelper;
use think\Controller;
use think\Db;
use think\facade\Config;
use think\facade\Request;
use think\facade\Response;
/**
* API基础控制器
*/
class Api extends Controller
{
/**
* 无需登录的方法,同时也就不需要鉴权了
* @var array
*/
protected $noNeedLogin = [];
/**
* 无需鉴权的方法,但需要登录
* @var array
*/
protected $noNeedRight = [];
/**
* 当前请求方法
* @var string
*/
protected $requestType = '';
/**
* 默认响应输出类型
* @var string
*/
protected $responseType = 'json';
/**
* 构造方法
*/
public function __construct()
{
parent::__construct();
// 请求类型
$this->requestType = Request::method();
// 控制器初始化
$this->_initialize();
// 跨域请求检测
$this->_checkCors();
}
/**
* 初始化操作
*/
protected function _initialize()
{
// 初始化操作
}
/**
* 跨域检测
* @deprecated 已由全局中间件 AllowCrossDomain 处理,此方法保留用于兼容
*/
protected function _checkCors()
{
// 由全局中间件处理跨域,此处不再处理
if ($this->requestType === 'OPTIONS') {
Response::create()->send();
exit;
}
}
/**
* 操作成功返回的数据
* @param string $msg 提示信息
* @param mixed $data 返回的数据
* @param int $code 错误码默认为1
* @param string $type 输出类型
* @param array $header 发送的header信息
*/
protected function success($msg = '', $data = null, $code = 1, $type = null, array $header = [])
{
$this->result($msg, $data, $code, $type, $header);
}
/**
* 操作失败返回的数据
* @param string $msg 提示信息
* @param mixed $data 返回的数据
* @param int $code 错误码默认为0
* @param string $type 输出类型
* @param array $header 发送的header信息
*/
protected function error($msg = '', $data = null, $code = 0, $type = null, array $header = [])
{
$this->result($msg, $data, $code, $type, $header);
}
/**
* 返回封装后的API数据
* @param string $msg 提示信息
* @param mixed $data 要返回的数据
* @param int $code 错误码默认为0
* @param string $type 输出类型支持json/xml/jsonp
* @param array $header 发送的header信息
*/
protected function result($msg, $data = null, $code = 0, $type = null, array $header = [])
{
$result = [
'code' => $code,
'msg' => $msg,
'time' => time(),
'data' => $data,
];
// 返回数据格式
$type = $type ?: $this->responseType;
// 发送响应
$response = Response::create($result, $type)->header($header);
$response->send();
exit;
}
public function uploadApp()
{
$type = $this->request->param('type', '');
if (empty($type)){
return ResponseHelper::error('参数缺失');
}
if (!in_array($type,['ckb','aiStore'])){
return ResponseHelper::error('参数错误');
}
$data = Db::name('app_version')
->field('version,downloadUrl,updateContent,forceUpdate')
->where(['type'=>$type])
->order('id DESC')
->find();
return ResponseHelper::success($data, '获取成功');
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace app\common\controller;
use think\Controller;
use think\facade\Request;
use app\common\model\Attachment as AttachmentModel;
use app\common\util\AliyunOSS;
class Attachment extends Controller
{
/**
* 上传文件
* @return \think\response\Json
*/
public function upload()
{
try {
// 获取上传文件
$file = Request::file('file');
if (!$file) {
return json([
'code' => 400,
'msg' => '请选择要上传的文件'
]);
}
// 验证文件
$validate = \think\facade\Validate::rule([
'file' => [
'fileSize' => 50485760, // 50MB
'fileExt' => 'jpg,jpeg,png,gif,doc,docx,pdf,zip,rar,mp4,mp3,csv,xlsx,xls,ppt,pptx,txt',
]
]);
if (!$validate->check(['file' => $file])) {
return json([
'code' => 400,
'msg' => $validate->getError()
]);
}
// 生成文件hash
$hashKey = md5_file($file->getRealPath());
// 检查文件是否已存在
$existFile = AttachmentModel::getByHashKey($hashKey);
if ($existFile) {
return json([
'code' => 200,
'msg' => '文件已存在',
'data' => [
'id' => $existFile['id'],
'name' => $existFile['name'],
'url' => $existFile['source'],
'size' => isset($existFile['size']) ? $existFile['size'] : 0
]
]);
}
// 生成OSS对象名称
$objectName = AliyunOSS::generateObjectName($file->getInfo('name'));
// 上传到OSS
$result = AliyunOSS::uploadFile($file->getRealPath(), $objectName);
if (!$result['success']) {
return json([
'code' => 500,
'msg' => '文件上传失败:' . $result['error']
]);
}
// 保存到数据库
$attachmentData = [
'name' => Request::param('name') ?: $file->getInfo('name'),
'hash_key' => $hashKey,
'server' => 'aliyun_oss',
'source' => $result['url'],
'size' => $result['size'],
'suffix' => pathinfo($file->getInfo('name'), PATHINFO_EXTENSION)
];
$attachment = AttachmentModel::addAttachment($attachmentData);
if (!$attachment) {
return json([
'code' => 500,
'msg' => '保存附件信息失败'
]);
}
return json([
'code' => 200,
'msg' => '上传成功',
'data' => [
'id' => $attachment->id,
'name' => $attachmentData['name'],
'url' => $attachmentData['source'],
'size' => $attachmentData['size']
]
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '上传失败:' . $e->getMessage()
]);
}
}
/**
* 获取附件信息
* @param int $id 附件ID
* @return \think\response\Json
*/
public function info($id)
{
try {
$attachment = AttachmentModel::find($id);
if (!$attachment) {
return json([
'code' => 404,
'msg' => '附件不存在'
]);
}
return json([
'code' => 200,
'msg' => '获取成功',
'data' => $attachment
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '获取失败:' . $e->getMessage()
]);
}
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace app\common\controller;
use app\common\service\AuthService;
use library\ResponseHelper;
use think\Controller;
use think\facade\Request;
/**
* 认证控制器
* 处理用户登录和身份验证
*/
class Auth extends Controller
{
/**
* 允许跨域请求的域名
* @var string
*/
protected $allowOrigin = '*';
/**
* 认证服务实例
* @var AuthService
*/
protected $authService;
/**
* 初始化
*/
public function initialize()
{
parent::initialize();
// 由全局中间件处理跨域,此处不再处理
// 初始化认证服务
$this->authService = new AuthService();
}
/**
* 用户登录
* @return \think\response\Json
*/
public function login()
{
// 获取登录参数
$params = Request::only(['account', 'password', 'typeId']);
// 参数验证
$validate = validate('common/Auth');
if (!$validate->scene('login')->check($params)) {
return ResponseHelper::error($validate->getError());
}
try {
// 调用登录服务
$result = $this->authService->login(
$params['account'],
$params['password'],
$params['typeId'],
Request::ip()
);
return ResponseHelper::success($result, '登录成功');
} catch (\Exception $e) {
return ResponseHelper::error($e->getMessage());
}
}
/**
* 手机号验证码登录
* @return \think\response\Json
*/
public function mobileLogin()
{
// 获取登录参数
$params = Request::only(['account', 'code', 'typeId']);
// 参数验证
$validate = validate('common/Auth');
if (!$validate->scene('mobile_login')->check($params)) {
return ResponseHelper::error($validate->getError());
}
try {
// 判断验证码是否已加密
$isEncrypted = isset($params['is_encrypted']) && $params['is_encrypted'] === true;
// 调用手机号登录服务
$result = $this->authService->mobileLogin(
$params['account'],
$params['code'],
Request::ip(),
$isEncrypted
);
return ResponseHelper::success($result, '登录成功');
} catch (\Exception $e) {
return ResponseHelper::error($e->getMessage());
}
}
/**
* 发送验证码
* @return \think\response\Json
*/
public function sendCode()
{
// 获取参数
$params = Request::only(['account', 'type']);
// 参数验证
$validate = validate('common/Auth');
if (!$validate->scene('send_code')->check($params)) {
return ResponseHelper::error($validate->getError());
}
try {
// 调用发送验证码服务
$result = $this->authService->sendLoginCode(
$params['account'],
$params['type']
);
return ResponseHelper::success($result, '验证码发送成功');
} catch (\Exception $e) {
return ResponseHelper::error($e->getMessage());
}
}
/**
* 获取用户信息
* @return \think\response\Json
*/
public function info()
{
try {
$result = $this->authService->getUserInfo(request()->userInfo);
return ResponseHelper::success($result);
} catch (\Exception $e) {
return ResponseHelper::unauthorized($e->getMessage());
}
}
/**
* 刷新令牌
* @return \think\response\Json
*/
public function refresh()
{
try {
$result = $this->authService->refreshToken(request()->userInfo);
return ResponseHelper::success($result, '刷新成功');
} catch (\Exception $e) {
return ResponseHelper::unauthorized($e->getMessage());
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace app\common\controller;
use think\Controller;
/**
* 基础控制器
*/
class BaseController extends Controller
{
}

View File

@@ -0,0 +1,333 @@
<?php
namespace app\common\controller;
use PHPExcel;
use PHPExcel_IOFactory;
use PHPExcel_Worksheet_Drawing;
use think\Controller;
use think\Exception;
/**
* 通用导出控制器,提供 Excel 导出与图片插入能力
*/
class ExportController extends Controller
{
/**
* @var array<string> 需要在请求结束时清理的临时文件
*/
protected static $tempFiles = [];
/**
* 导出 Excel支持指定列插入图片
*
* @param string $fileName 输出文件名(可不带扩展名)
* @param array $headers 列定义,例如 ['name' => '姓名', 'phone' => '电话']
* @param array $rows 数据行,需与 $headers 的 key 对应
* @param array $imageColumns 需要渲染为图片的列 key 列表
* @param string $sheetName 工作表名称
* @param array $options 额外选项:
* - imageWidth(图片宽度默认100)
* - imageHeight(图片高度默认100)
* - imageColumnWidth(图片列宽默认15)
* - titleRow(标题行内容,支持多行文本数组)
*
* @throws Exception
*/
public static function exportExcelWithImages(
$fileName,
array $headers,
array $rows,
array $imageColumns = [],
$sheetName = 'Sheet1',
array $options = []
) {
if (empty($headers)) {
throw new Exception('导出列定义不能为空');
}
if (empty($rows)) {
throw new Exception('导出数据不能为空');
}
// 抑制 PHPExcel 库中已废弃的大括号语法警告PHP 7.4+
$oldErrorReporting = error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
// 默认选项
$imageWidth = isset($options['imageWidth']) ? (int)$options['imageWidth'] : 100;
$imageHeight = isset($options['imageHeight']) ? (int)$options['imageHeight'] : 100;
$imageColumnWidth = isset($options['imageColumnWidth']) ? (float)$options['imageColumnWidth'] : 15;
$rowHeight = isset($options['rowHeight']) ? (int)$options['rowHeight'] : ($imageHeight + 10);
$excel = new PHPExcel();
$sheet = $excel->getActiveSheet();
$sheet->setTitle($sheetName);
$columnKeys = array_keys($headers);
$totalColumns = count($columnKeys);
$lastColumnLetter = self::columnLetter($totalColumns - 1);
// 定义特定列的固定宽度(如果未指定则使用默认值)
$columnWidths = isset($options['columnWidths']) ? $options['columnWidths'] : [];
// 检查是否有标题行
$titleRow = isset($options['titleRow']) ? $options['titleRow'] : null;
$dataStartRow = 1; // 数据开始行(表头行)
// 如果有标题行,先写入标题行(支持数组或字符串)
if (!empty($titleRow)) {
$dataStartRow = 2; // 数据从第2行开始第1行是标题第2行是表头
// 合并标题行单元格(从第一列到最后一列)
$titleRange = 'A1:' . $lastColumnLetter . '1';
$sheet->mergeCells($titleRange);
// 构建标题内容(支持多行数组或字符串)
$titleContent = '';
if (is_array($titleRow)) {
$titleContent = implode("\n", $titleRow);
} else {
$titleContent = (string)$titleRow;
}
// 写入标题
$sheet->setCellValue('A1', $titleContent);
// 设置标题行样式
$sheet->getStyle('A1')->applyFromArray([
'font' => ['bold' => true, 'size' => 16],
'alignment' => [
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
'wrap' => true
],
'fill' => [
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
'color' => ['rgb' => 'FFF8DC'] // 浅黄色背景
],
'borders' => [
'allborders' => [
'style' => \PHPExcel_Style_Border::BORDER_THIN,
'color' => ['rgb' => '000000']
]
]
]);
$sheet->getRowDimension(1)->setRowHeight(80); // 标题行高度
}
// 写入表头并设置列宽
$headerRow = $dataStartRow;
foreach ($columnKeys as $index => $key) {
$columnLetter = self::columnLetter($index);
$sheet->setCellValue($columnLetter . $headerRow, $headers[$key]);
// 如果是图片列,设置固定列宽
if (in_array($key, $imageColumns, true)) {
$sheet->getColumnDimension($columnLetter)->setWidth($imageColumnWidth);
} elseif (isset($columnWidths[$key])) {
// 如果指定了该列的宽度,使用指定宽度
$sheet->getColumnDimension($columnLetter)->setWidth($columnWidths[$key]);
} else {
// 否则自动调整
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
}
}
// 设置表头样式
$headerRange = 'A' . $headerRow . ':' . $lastColumnLetter . $headerRow;
$sheet->getStyle($headerRange)->applyFromArray([
'font' => ['bold' => true, 'size' => 11],
'alignment' => [
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
'wrap' => true
],
'fill' => [
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
'color' => ['rgb' => 'FFF8DC']
],
'borders' => [
'allborders' => [
'style' => \PHPExcel_Style_Border::BORDER_THIN,
'color' => ['rgb' => '000000']
]
]
]);
$sheet->getRowDimension($headerRow)->setRowHeight(30); // 增加表头行高以确保文本完整显示
// 写入数据与图片
$dataRowStart = $dataStartRow + 1; // 数据从表头行下一行开始
foreach ($rows as $rowIndex => $rowData) {
$excelRow = $dataRowStart + $rowIndex; // 数据行
$maxRowHeight = $rowHeight; // 记录当前行的最大高度
foreach ($columnKeys as $colIndex => $key) {
$columnLetter = self::columnLetter($colIndex);
$cell = $columnLetter . $excelRow;
$value = isset($rowData[$key]) ? $rowData[$key] : '';
if (in_array($key, $imageColumns, true) && !empty($value)) {
$imagePath = self::resolveImagePath($value);
if ($imagePath) {
// 获取图片实际尺寸并等比例缩放
$imageSize = @getimagesize($imagePath);
if ($imageSize) {
$originalWidth = $imageSize[0];
$originalHeight = $imageSize[1];
// 计算等比例缩放后的尺寸
$ratio = min($imageWidth / $originalWidth, $imageHeight / $originalHeight);
$scaledWidth = $originalWidth * $ratio;
$scaledHeight = $originalHeight * $ratio;
// 确保不超过最大尺寸
if ($scaledWidth > $imageWidth) {
$scaledWidth = $imageWidth;
$scaledHeight = $originalHeight * ($imageWidth / $originalWidth);
}
if ($scaledHeight > $imageHeight) {
$scaledHeight = $imageHeight;
$scaledWidth = $originalWidth * ($imageHeight / $originalHeight);
}
$drawing = new PHPExcel_Worksheet_Drawing();
$drawing->setPath($imagePath);
$drawing->setCoordinates($cell);
// 居中显示图片Excel列宽1单位≈7像素行高1单位≈0.75像素)
$cellWidthPx = $imageColumnWidth * 7;
$cellHeightPx = $maxRowHeight * 0.75;
$offsetX = max(2, ($cellWidthPx - $scaledWidth) / 2);
$offsetY = max(2, ($cellHeightPx - $scaledHeight) / 2);
$drawing->setOffsetX((int)$offsetX);
$drawing->setOffsetY((int)$offsetY);
$drawing->setWidth((int)$scaledWidth);
$drawing->setHeight((int)$scaledHeight);
$drawing->setWorksheet($sheet);
// 更新行高以适应图片(留出一些边距)
$neededHeight = (int)($scaledHeight / 0.75) + 10;
if ($neededHeight > $maxRowHeight) {
$maxRowHeight = $neededHeight;
}
} else {
// 如果无法获取图片尺寸,使用默认尺寸
$drawing = new PHPExcel_Worksheet_Drawing();
$drawing->setPath($imagePath);
$drawing->setCoordinates($cell);
$drawing->setOffsetX(5);
$drawing->setOffsetY(5);
$drawing->setWidth($imageWidth);
$drawing->setHeight($imageHeight);
$drawing->setWorksheet($sheet);
}
} else {
$sheet->setCellValue($cell, '');
}
} else {
$sheet->setCellValue($cell, $value);
// 设置文本对齐和换行
$style = $sheet->getStyle($cell);
$style->getAlignment()->setVertical(\PHPExcel_Style_Alignment::VERTICAL_CENTER);
$style->getAlignment()->setWrapText(true);
// 根据列类型设置水平对齐
if (in_array($key, ['date', 'postTime'])) {
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
} else {
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
}
}
}
// 设置行高
$sheet->getRowDimension($excelRow)->setRowHeight($maxRowHeight);
}
$safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His'));
if (stripos($safeName, '.xlsx') === false) {
$safeName .= '.xlsx';
}
if (ob_get_length()) {
ob_end_clean();
}
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Cache-Control: max-age=0');
header('Content-Disposition: attachment;filename="' . $safeName . '"');
try {
$writer = PHPExcel_IOFactory::createWriter($excel, 'Excel2007');
$writer->save('php://output');
} catch (\Exception $e) {
// 恢复错误报告级别
error_reporting($oldErrorReporting);
throw $e;
}
// 恢复错误报告级别
error_reporting($oldErrorReporting);
self::cleanupTempFiles();
exit;
}
/**
* 根据列序号生成 Excel 列字母
*
* @param int $index
* @return string
*/
protected static function columnLetter($index)
{
$letters = '';
do {
$letters = chr($index % 26 + 65) . $letters;
$index = intval($index / 26) - 1;
} while ($index >= 0);
return $letters;
}
/**
* 将远程或本地图片路径转换为可用的本地文件路径
*
* @param string $path
* @return string|null
*/
protected static function resolveImagePath($path)
{
if (empty($path)) {
return null;
}
if (preg_match('/^https?:\/\//i', $path)) {
$tempFile = tempnam(sys_get_temp_dir(), 'export_img_');
$stream = @file_get_contents($path);
if ($stream === false) {
return null;
}
file_put_contents($tempFile, $stream);
self::$tempFiles[] = $tempFile;
return $tempFile;
}
if (file_exists($path)) {
return $path;
}
return null;
}
/**
* 清理所有临时文件
*/
protected static function cleanupTempFiles()
{
foreach (self::$tempFiles as $file) {
if (file_exists($file)) {
@unlink($file);
}
}
self::$tempFiles = [];
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace app\common\controller;
use EasyWeChat\Factory;
use think\Controller;
use think\facade\Env;
class GetOpenid extends Controller
{
protected $app;
public function __construct()
{
parent::__construct();
// 从环境变量获取配置
$config = [
'app_id' => Env::get('weChat.appid'),
'secret' => Env::get('weChat.secret'),
'response_type' => 'array'
];
$this->app = Factory::officialAccount($config);
}
public function index()
{
$app = $this->app;
$oauth = $app->oauth;
// 未登录
if (empty($_SESSION['wechat_user'])) {
$_SESSION['target_url'] = 'user/profile';
$redirectUrl = $oauth->redirect();
exit_data($redirectUrl);
header("Location: {$redirectUrl}");
exit;
}
// 已经登录过
$user = $_SESSION['wechat_user'];
exit_data($user);
return 'Hello, World!';
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace app\common\controller;
use app\common\model\User as UserModel;
use app\common\util\JwtUtil;
use Exception;
use library\ResponseHelper;
use think\Db;
use think\Validate;
/**
* 认证控制器
* 处理用户登录和身份验证
*/
class PasswordLoginController extends BaseController
{
/**
* 获取用户基本信息
*
* @param string $account
* @param int $typeId
* @return UserModel
*/
protected function getUserProfileWithAccountAndType(string $account, int $typeId)
{
$user = UserModel::where(
function ($query) use ($account) {
$query->where('phone', $account)->whereOr('account', $account);
}
)
->where(
function ($query) use ($typeId) {
$query->where('status', 1)->where('typeId', $typeId);
}
)->find();
if(!empty($user)){
return $user;
}else{
return '';
}
}
/**
* 获取用户信息
*
* @param string $account 账号(手机号)
* @param string $password 密码(可能是加密后的)
* @param int $typeId 身份信息
* @return array|null
*/
protected function getUser(string $account, string $password, int $typeId): array
{
$user = $this->getUserProfileWithAccountAndType($account, $typeId);
if (!$user) {
throw new \Exception('用户不存在或已禁用', 403);
}
$password = md5($password);
if ($user->passwordMd5 !== $password) {
throw new \Exception('账号或密码错误', 403);
}
return array_merge($user->toArray(), [
'lastLoginIp' => $this->request->ip(),
'lastLoginTime' => time()
]);
}
/**
* 数据验证
*
* @param array $params
* @return $this
* @throws \Exception
*/
protected function dataValidate(array $params): self
{
$validate = Validate::make([
'account' => 'require',
'password' => 'require|length:6,64',
'typeId' => 'require|in:1,2',
], [
'account.require' => '账号不能为空',
'password.require' => '密码不能为空',
'password.length' => '密码长度必须在6-64个字符之间',
'typeId.require' => '用户类型不能为空',
'typeId.in' => '用户类型错误',
]);
if (!$validate->check($params)) {
throw new \Exception($validate->getError(), 400);
}
return $this;
}
/**
* 用户登录
*
* @param string $account 账号(手机号)
* @param string $password 密码(可能是加密后的)
* @param string $typeId 登录IP
* @param string $deviceId 本地设备imei
* @return array
* @throws \Exception
*/
protected function doLogin(string $account, string $password, int $typeId, string $deviceId): array
{
// 获取用户信息
$member = $this->getUser($account, $password, $typeId);
$deviceTotal = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0])->count();
//更新设备imei
if ($typeId == 2 && !empty($deviceId)){
$deviceUser = Db::name('device_user')->where(['companyId' => $member['companyId'],'userId' => $member['id'],'deleteTime' => 0])->find();
if (!empty($deviceUser)){
$device = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0,'id' => $deviceUser['deviceId']])->find();
if (!empty($device) && empty($device['deviceImei'])){
Db::table('s2_device')->where(['id' => $device['id']])->update(['deviceImei' => $deviceId,'updateTime' => time()]);
Db::name('device')->where(['id' => $device['id']])->update(['deviceImei' => $deviceId,'updateTime' => time()]);
}
}
}
// 生成JWT令牌
$token = JwtUtil::createToken($member, 86400 * 30);
$token_expired = time() + 86400 * 30;
return compact('member', 'token', 'token_expired','deviceTotal');
}
/**
* 用户登录
*
* @return \think\response\Json
*/
public function index()
{
$params = $this->request->only(['account', 'password', 'typeId','deviceId']);
try {
$deviceId = isset($params['deviceId']) ? $params['deviceId'] : '';
$userData = $this->dataValidate($params)->doLogin(
$params['account'],
$params['password'],
$params['typeId'],
$deviceId
);
return ResponseHelper::success($userData, '登录成功');
} catch (Exception $e) {
return ResponseHelper::error($e->getMessage(), $e->getCode());
}
}
}

View File

@@ -0,0 +1,543 @@
<?php
namespace app\common\controller;
use app\chukebao\model\TokensCompany;
use app\chukebao\model\TokensRecord;
use think\Db;
use app\common\util\PaymentUtil;
use think\facade\Env;
use think\facade\Request;
use app\common\model\Order;
use app\common\model\User;
/**
* 支付服务(内部调用)
*/
class PaymentService
{
/**
* 统一支付下单接口
* 支持扫码付款、微信支付、支付宝支付
*
* @param array $order
* - orderNo: string 商户订单号(必填)
* - money: int 金额(分,必填)
* - goodsName: string 商品描述(必填)
* - service: string 支付服务类型(可选)
* - 'wechat' 或 'pay.weixin.jspay': 微信JSAPI支付
* - 'alipay' 或 'pay.alipay.jspay': 支付宝JSAPI支付
* - 不传或空: 默认扫码付款
* - openid: string 微信用户openid微信JSAPI支付必填
* - buyer_id: string 支付宝用户ID支付宝JSAPI支付可选
* - notify_url: string 异步通知地址(可选)
* @return string JSON格式响应
*/
public function createOrder(array $order)
{
// 确定service类型支持简写形式 wechat/alipay或完整的 service 值
$serviceType = $order['service'] ?? '';
// 映射简写形式到完整的 service 值
if ($serviceType === 'wechat' || $serviceType === 'pay.weixin.jspay') {
$service = 'pay.weixin.jspay';
} elseif ($serviceType === 'alipay' || $serviceType === 'pay.alipay.jspay') {
$service = 'pay.alipay.jspay';
} elseif ($serviceType === 'qrCode' || $serviceType === 'unified.trade.native') {
$service = 'unified.trade.native';
} else {
// 默认扫码支付
$service = 'unified.trade.native';
}
// 构建基础参数
$params = [
'service' => $service,
'sign_type' => PaymentUtil::SIGN_TYPE_MD5,
'mch_id' => Env::get('payment.mchId'),
'out_trade_no' => $order['orderNo'],
'body' => $order['goodsName'] ?? '',
'total_fee' => $order['money'] ?? 0,
'mch_create_ip' => Request::ip(),
'notify_url' => $order['notify_url'] ?? Env::get('payment.notify_url', '127.0.0.1'),
'nonce_str' => PaymentUtil::generateNonceStr(),
];
// 微信JSAPI支付需要openid
if ($service == 'pay.weixin.jspay') {
// $params['sub_openid'] = 'oB44Yw1T6bfVAZwjj729P-6CUSPE';
$params['is_raw'] = 0;
$params['mch_app_name'] = '存客宝';
$params['mch_app_id'] = 'https://kr-op.quwanzhi.com';
}
// 支付宝JSAPI支付需要buyer_id可选
if ($service == 'pay.alipay.jspay') {
$params['is_raw'] = 0;
$params['quit_url'] = $params['notify_url'];
$params['buyer_id'] = '';
}
Db::startTrans();
try {
// 签名
$secret = Env::get('payment.key');
$params['sign_type'] = 'MD5';
$params['sign'] = PaymentUtil::generateSign($params, $secret, 'MD5');
$url = Env::get('payment.url');
if (empty($url)) {
throw new \Exception('支付网关地址未配置');
}
// 创建订单
Order::create([
'mchId' => $params['mch_id'],
'companyId' => isset($order['companyId']) ? $order['companyId'] : 0,
'userId' => isset($order['userId']) ? $order['userId'] : 0,
'orderType' => isset($order['orderType']) ? $order['orderType'] : 1,
'status' => 0,
'goodsId' => isset($order['goodsId']) ? $order['goodsId'] : 0,
'goodsName' => isset($order['goodsName']) ? $order['goodsName'] : '',
'money' => isset($order['money']) ? $order['money'] : 0,
'goodsSpecs' => isset($order['goodsSpecs']) ? json_encode($order['goodsSpecs'], 256) : json_encode([]),
'orderNo' => isset($order['orderNo']) ? $order['orderNo'] : '',
'ip' => Request::ip(),
'nonceStr' => isset($order['nonceStr']) ? $order['nonceStr'] : '',
'createTime' => time(),
]);
// XML POST 请求
$xmlBody = $this->arrayToXml($params);
$response = $this->postXml($url, $xmlBody);
$parsed = $this->parseXmlOrRaw($response);
if ($parsed['status'] == 0 && $parsed['result_code'] == 0) {
Db::commit();
// 根据service类型返回不同的数据格式仅返回接口文档中的字段
$responseData = null;
if ($service == 'unified.trade.native') {
// 扫码支付返回二维码URL
$responseData = $parsed['code_img_url'] ?? '';
} elseif ($service == 'pay.weixin.jspay') {
// 微信JSAPI支付返回支付参数仅返回接口文档中存在的字段
$responseData = [];
if (isset($parsed['appid'])) $responseData['appid'] = $parsed['appid'];
if (isset($parsed['time_stamp'])) $responseData['time_stamp'] = $parsed['time_stamp'];
if (isset($parsed['nonce_str'])) $responseData['nonce_str'] = $parsed['nonce_str'];
if (isset($parsed['package'])) $responseData['package'] = $parsed['package'];
if (isset($parsed['sign_type'])) $responseData['sign_type'] = $parsed['sign_type'];
if (isset($parsed['pay_sign'])) $responseData['pay_sign'] = $parsed['pay_sign'];
} elseif ($service == 'pay.alipay.jspay') {
// 支付宝JSAPI支付返回订单信息仅返回接口文档中存在的字段
$responseData = [];
if (isset($parsed['order_info'])) $responseData['order_info'] = $parsed['order_info'];
if (isset($parsed['order_string'])) $responseData['order_string'] = $parsed['order_string'];
}
return json_encode(['code' => 200, 'msg' => '订单创建成功', 'data' => $responseData]);
} else {
Db::rollback();
return json_encode(['code' => 500, 'msg' => '订单创建失败:' . ($parsed['err_msg'] ?? '未知错误')]);
}
} catch (\Exception $e) {
Db::rollback();
return json_encode(['code' => 500, 'msg' => '订单创建失败:' . $e->getMessage()]);
}
}
/**
* POST 请求x-www-form-urlencoded
*/
protected function httpPost(string $url, array $params, array $headers = [])
{
if (!function_exists('requestCurl')) {
throw new \RuntimeException('requestCurl 未定义');
}
return requestCurl($url, $params, 'POST', $headers, 'dataBuild');
}
/**
* 解析响应
*/
protected function parseResponse($response)
{
if ($response === '' || $response === null) {
return '';
}
$decoded = json_decode($response, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
if (strpos($response, '=') !== false && strpos($response, '&') !== false) {
$arr = [];
foreach (explode('&', $response) as $pair) {
if ($pair === '') continue;
$kv = explode('=', $pair, 2);
$arr[$kv[0]] = $kv[1] ?? '';
}
return $arr;
}
return $response;
}
/**
* 以 XML 方式 POSTtext/xml
*/
protected function postXml(string $url, string $xml)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: text/xml; charset=UTF-8'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$res = curl_exec($ch);
curl_close($ch);
return $res;
}
/**
* 数组转 XML按 ASCII 升序,字符串走 CDATA
*/
protected function arrayToXml(array $data): string
{
// 过滤空值
$filtered = [];
foreach ($data as $k => $v) {
if ($v === '' || $v === null) continue;
$filtered[$k] = $v;
}
ksort($filtered, SORT_STRING);
$xml = '<xml>';
foreach ($filtered as $key => $value) {
if (is_numeric($value)) {
$xml .= "<{$key}>{$value}</{$key}>";
} else {
$xml .= "<{$key}><![CDATA[{$value}]]></{$key}>";
}
}
$xml .= '</xml>';
return $xml;
}
/**
* 解析 XML 响应
*/
protected function parseXmlOrRaw($response)
{
if (!is_string($response) || $response === '') {
return $response;
}
libxml_use_internal_errors(true);
$xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA);
if ($xml !== false) {
$json = json_encode($xml, JSON_UNESCAPED_UNICODE);
return json_decode($json, true);
}
return $response;
}
/**
* 支付结果异步通知
* - 威富通回调为 XML需校验签名与业务字段并更新订单
* - 支持扫码付款、微信支付、支付宝支付的通知
* - 回应成功返回XML格式SUCCESS失败返回XML格式FAIL
* @return string XML响应
*/
public function notify()
{
$rawBody = file_get_contents('php://input');
$payload = $this->parseXmlOrRaw($rawBody);
if (!is_array($payload) || empty($payload)) {
\think\facade\Log::error('支付通知XML解析错误', ['rawBody' => $rawBody]);
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[XML解析错误]]></return_msg></xml>';
}
// 验证签名
$secret = Env::get('payment.key');
if (!empty($secret) && isset($payload['sign'])) {
$signType = $payload['sign_type'] ?? 'MD5';
if (!PaymentUtil::verifySign($payload, $secret, $signType)) {
\think\facade\Log::error('支付通知:签名验证失败', ['payload' => $payload]);
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名验证失败]]></return_msg></xml>';
}
}
// 检查通信状态
if (isset($payload['status']) && $payload['status'] != 0) {
$errMsg = $payload['err_msg'] ?? '通信失败';
\think\facade\Log::error('支付通知:通信失败', ['payload' => $payload]);
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . $errMsg . ']]></return_msg></xml>';
}
// 检查业务结果
if (isset($payload['result_code']) && $payload['result_code'] != 0) {
$errMsg = $payload['err_msg'] ?? '业务处理失败';
\think\facade\Log::error('支付通知:业务处理失败', ['payload' => $payload]);
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . $errMsg . ']]></return_msg></xml>';
}
// 业务处理:更新订单
Db::startTrans();
try {
$outTradeNo = $payload['out_trade_no'] ?? '';
$pay_result = $payload['pay_result'] ?? 0;
$time_end = $payload['time_end'] ?? '';
if (empty($outTradeNo)) {
Db::rollback();
\think\facade\Log::error('支付通知:订单号为空', ['payload' => $payload]);
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单号为空]]></return_msg></xml>';
}
$order = Order::where('orderNo', $outTradeNo)->find();
if (!$order) {
Db::rollback();
\think\facade\Log::error('支付通知:订单不存在', ['out_trade_no' => $outTradeNo]);
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单不存在]]></return_msg></xml>';
}
// 如果订单已支付,直接返回成功(防止重复处理)
if ($order->status == 1) {
Db::rollback();
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
}
if ($pay_result != 0) {
$order->payInfo = $payload['pay_info'] ?? '支付失败';
$order->status = 3;
$order->save();
Db::commit();
\think\facade\Log::error('支付通知:支付失败', ['orderNo' => $outTradeNo, 'pay_info' => $payload['pay_info'] ?? '']);
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . ($payload['pay_info'] ?? '支付失败') . ']]></return_msg></xml>';
}
// 根据trade_type判断支付方式
$tradeType = $payload['trade_type'] ?? '';
if (strpos($tradeType, 'wechat') !== false || strpos($tradeType, 'weixin') !== false) {
$order->payType = 1; // 微信支付
} elseif (strpos($tradeType, 'alipay') !== false) {
$order->payType = 2; // 支付宝支付
} else {
// 默认根据原有逻辑判断
$order->payType = $tradeType == 'pay.wechat.jspay' ? 1 : 2;
}
$order->status = 1;
$order->payTime = $this->parsePayTime($time_end);
$order->transactionId = $payload['transaction_id'] ?? '';
$order->save();
//订单处理
$this->processOrder($order);
Db::commit();
// 返回成功响应XML格式
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
} catch (\Exception $e) {
Db::rollback();
\think\facade\Log::error('支付通知:处理异常', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理异常]]></return_msg></xml>';
}
}
/**
* 解析威富通时间yyyyMMddHHmmss为时间戳
*/
protected function parsePayTime(string $timeEnd)
{
if ($timeEnd === '') {
return 0;
}
// 期望格式20250102153045
if (preg_match('/^\\d{14}$/', $timeEnd) !== 1) {
return 0;
}
$dt = \DateTime::createFromFormat('YmdHis', $timeEnd, new \DateTimeZone('Asia/Shanghai'));
return $dt ? $dt->getTimestamp() : 0;
}
/**
* 查询订单(威富通 unified.trade.query
* - 入参:商户订单号或平台交易号
* - 出参:统一 JSON 格式,包含交易状态与关键信息
* @param array $query
* - out_trade_no: string 商户订单号(与 transaction_id 二选一)
* - transaction_id: string 平台交易号(与 out_trade_no 二选一)
* @return \think\response\Json
*/
public function queryOrder($orderNo = '')
{
if (empty($orderNo)) {
return json(['code' => 422, 'msg' => '订单号缺失']);
}
$params = [
'service' => 'unified.trade.query',
'mch_id' => Env::get('payment.mchId'),
'out_trade_no' => $orderNo ?: null,
'nonce_str' => PaymentUtil::generateNonceStr(),
'sign_type' => 'MD5',
];
// 过滤空值后签名
$secret = Env::get('payment.key');
if (empty($secret)) {
return json_encode(['code' => 500, 'msg' => '支付密钥未配置']);
}
$filtered = [];
foreach ($params as $k => $v) {
if ($v === '' || $v === null) continue;
$filtered[$k] = $v;
}
$filtered['sign'] = PaymentUtil::generateSign($filtered, $secret, $filtered['sign_type']);
$url = Env::get('payment.url');
if (empty($url)) {
return json_encode(['code' => 500, 'msg' => '支付网关地址未配置']);
}
// 请求网关
$xmlBody = $this->arrayToXml($filtered);
$response = $this->postXml($url, $xmlBody);
$parsed = $this->parseXmlOrRaw($response);
if (!is_array($parsed)) {
return json_encode(['code' => 500, 'msg' => '响应解析失败', 'data' => $response]);
}
if ($parsed['status'] != 0) {
return json_encode(['code' => 500, 'msg' => '通信失败']);
}
if ($parsed['result_code'] == 0) {
$order = Order::where('orderNo', $orderNo)->lock(true)->find();
if (empty($order)) {
return json_encode(['code' => 500, 'msg' => '订单不存在']);
}
if ($order['status'] == 1) {
return json_encode(['code' => 200, 'msg' => '支付成功']);
}
$tradeState = $parsed['trade_state'] ?? '';
$resp = [
'trade_state' => $tradeState,
'trade_state_desc' => $parsed['trade_state_desc'] ?? '',
'transaction_id' => $parsed['transaction_id'] ?? '',
'out_trade_no' => $parsed['out_trade_no'] ?? $orderNo,
'total_fee' => isset($parsed['total_fee']) ? (int)$parsed['total_fee'] : null,
'time_end' => $parsed['time_end'] ?? '',
'buyer_logon_id' => $parsed['buyer_logon_id'] ?? '',
'bank_type' => $parsed['bank_type'] ?? '',
];
// 若已支付,同步本地订单
if ($tradeState == 'SUCCESS') {
Db::startTrans();
try {
/** @var Order|null $order */
$order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find();
if ($order) {
$paidAt = $this->parsePayTime($resp['time_end'] ?? '') ?: time();
if ($order['status'] != 1) {
$order->save([
'status' => 1,
'transactionId' => $resp['transaction_id'] ?? '',
'payTime' => $paidAt,
'updateTime' => time(),
]);
}
}
//订单处理
$this->processOrder($order);
Db::commit();
return json_encode(['code' => 200, 'msg' => '支付成功']);
} catch (\Exception $e) {
Db::rollback();
return json_encode(['code' => 500, 'msg' => '付款失败' . $e->getMessage()]);
}
} else {
$order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find();
if ($order) {
$order->status = 3;
$order->payInfo = $resp['trade_state_desc'] ?? '';
$order->save();
}
return json_encode(['code' => 500, 'msg' => '支付失败', 'data' => $resp]);
}
} else {
return json_encode(['code' => 500, 'msg' => '通信失败']);
}
}
public function processOrder($order = [])
{
if (empty($order)) {
return false;
}
switch ($order['orderType']) {
case 1:
// 处理购买算力
// 查询用户信息判断是否为管理员需要同时匹配userId和companyId
$user = User::where([
'id' => $order->userId,
'companyId' => $order->companyId
])->find();
$isAdmin = (!empty($user) && isset($user->isAdmin) && $user->isAdmin == 1) ? 1 : 0;
$token = TokensCompany::where(['companyId' => $order->companyId,'userId' => $order->userId])->find();
$goodsSpecs = json_decode($order->goodsSpecs, true);
if (!empty($token)) {
$token->tokens = $token->tokens + $goodsSpecs['tokens'];
$token->updateTime = time();
$token->save();
$newTokens = $token->tokens;
} else {
$tokensCompany = new TokensCompany();
$tokensCompany->userId = $order->userId;
$tokensCompany->companyId = $order->companyId;
$tokensCompany->tokens = $goodsSpecs['tokens'];
$tokensCompany->isAdmin = $isAdmin;
$tokensCompany->createTime = time();
$tokensCompany->updateTime = time();
$tokensCompany->save();
$newTokens = $tokensCompany->tokens;
}
//添加记录
$record = new TokensRecord();
$record->companyId = $order->companyId;
$record->userId = $order->userId;
$record->type = 1;
$record->form = 5;
$record->wechatAccountId = 0;
$record->friendIdOrGroupId = 0;
$record->remarks = '购买算力【' . $goodsSpecs['name'] . '】';
$record->tokens = $goodsSpecs['tokens'];
$record->balanceTokens = $newTokens;
$record->createTime = time();
$record->save();
break;
}
return true;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace app\common\controller;
use library\ResponseHelper;
use think\facade\Request;
/**
* 认证控制器
* 处理用户登录和身份验证
*/
class SendCodeController extends BaseController
{
/**
* 发送验证码
* @return \think\response\Json
*/
public function index()
{
$params = $this->request->only(['account', 'type']);
// 参数验证
$validate = validate('common/Auth');
if (!$validate->scene('send_code')->check($params)) {
return ResponseHelper::error($validate->getError());
}
try {
// 调用发送验证码服务
$result = $this->authService->sendLoginCode(
$params['account'],
$params['type']
);
return ResponseHelper::success($result, '验证码发送成功');
} catch (\Exception $e) {
return ResponseHelper::error($e->getMessage());
}
}
}