feat: 初始化进销存数据开放平台项目
初始化完整的前后端项目结构,包含ThinkPHP后端API服务和Next.js前端管理系统,添加基础配置、中间件、模型、路由等核心代码,以及项目文档和静态资源。
This commit is contained in:
1
thinkphp/app/.htaccess
Normal file
1
thinkphp/app/.htaccess
Normal file
@@ -0,0 +1 @@
|
||||
deny from all
|
||||
22
thinkphp/app/AppService.php
Normal file
22
thinkphp/app/AppService.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app;
|
||||
|
||||
use think\Service;
|
||||
|
||||
/**
|
||||
* 应用服务类
|
||||
*/
|
||||
class AppService extends Service
|
||||
{
|
||||
public function register()
|
||||
{
|
||||
// 服务注册
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
// 服务启动
|
||||
}
|
||||
}
|
||||
94
thinkphp/app/BaseController.php
Normal file
94
thinkphp/app/BaseController.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app;
|
||||
|
||||
use think\App;
|
||||
use think\exception\ValidateException;
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* 控制器基础类
|
||||
*/
|
||||
abstract class BaseController
|
||||
{
|
||||
/**
|
||||
* Request实例
|
||||
* @var \think\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* 应用实例
|
||||
* @var \think\App
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* 是否批量验证
|
||||
* @var bool
|
||||
*/
|
||||
protected $batchValidate = false;
|
||||
|
||||
/**
|
||||
* 控制器中间件
|
||||
* @var array
|
||||
*/
|
||||
protected $middleware = [];
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
* @access public
|
||||
* @param App $app 应用对象
|
||||
*/
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->request = $this->app->request;
|
||||
|
||||
// 控制器初始化
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 验证数据
|
||||
* @access protected
|
||||
* @param array $data 数据
|
||||
* @param string|array $validate 验证器名或者验证规则数组
|
||||
* @param array $message 提示信息
|
||||
* @param bool $batch 是否批量验证
|
||||
* @return array|string|true
|
||||
* @throws ValidateException
|
||||
*/
|
||||
protected function validate(array $data, $validate, array $message = [], bool $batch = false)
|
||||
{
|
||||
if (is_array($validate)) {
|
||||
$v = new Validate();
|
||||
$v->rule($validate);
|
||||
} else {
|
||||
if (strpos($validate, '.')) {
|
||||
// 支持场景
|
||||
[$validate, $scene] = explode('.', $validate);
|
||||
}
|
||||
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
|
||||
$v = new $class();
|
||||
if (!empty($scene)) {
|
||||
$v->scene($scene);
|
||||
}
|
||||
}
|
||||
|
||||
$v->message($message);
|
||||
|
||||
// 是否批量验证
|
||||
if ($batch || $this->batchValidate) {
|
||||
$v->batch(true);
|
||||
}
|
||||
|
||||
return $v->failException(true)->check($data);
|
||||
}
|
||||
|
||||
}
|
||||
58
thinkphp/app/ExceptionHandle.php
Normal file
58
thinkphp/app/ExceptionHandle.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
namespace app;
|
||||
|
||||
use think\db\exception\DataNotFoundException;
|
||||
use think\db\exception\ModelNotFoundException;
|
||||
use think\exception\Handle;
|
||||
use think\exception\HttpException;
|
||||
use think\exception\HttpResponseException;
|
||||
use think\exception\ValidateException;
|
||||
use think\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 应用异常处理类
|
||||
*/
|
||||
class ExceptionHandle extends Handle
|
||||
{
|
||||
/**
|
||||
* 不需要记录信息(日志)的异常类列表
|
||||
* @var array
|
||||
*/
|
||||
protected $ignoreReport = [
|
||||
HttpException::class,
|
||||
HttpResponseException::class,
|
||||
ModelNotFoundException::class,
|
||||
DataNotFoundException::class,
|
||||
ValidateException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* 记录异常信息(包括日志或者其它方式记录)
|
||||
*
|
||||
* @access public
|
||||
* @param Throwable $exception
|
||||
* @return void
|
||||
*/
|
||||
public function report(Throwable $exception): void
|
||||
{
|
||||
// 使用内置的方式记录异常日志
|
||||
parent::report($exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @access public
|
||||
* @param \think\Request $request
|
||||
* @param Throwable $e
|
||||
* @return Response
|
||||
*/
|
||||
public function render($request, Throwable $e): Response
|
||||
{
|
||||
// 添加自定义异常处理机制
|
||||
|
||||
// 其他错误交给系统处理
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
}
|
||||
8
thinkphp/app/Request.php
Normal file
8
thinkphp/app/Request.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace app;
|
||||
|
||||
// 应用请求对象类
|
||||
class Request extends \think\Request
|
||||
{
|
||||
|
||||
}
|
||||
2
thinkphp/app/common.php
Normal file
2
thinkphp/app/common.php
Normal file
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// 应用公共文件
|
||||
17
thinkphp/app/controller/Index.php
Normal file
17
thinkphp/app/controller/Index.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace app\controller;
|
||||
|
||||
use app\BaseController;
|
||||
|
||||
class Index extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V' . \think\facade\App::version() . '<br/><span style="font-size:30px;">16载初心不改 - 你值得信赖的PHP框架</span></p><span style="font-size:25px;">[ V6.0 版本由 <a href="https://www.yisu.com/" target="yisu">亿速云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ee9b1aa918103c4fc"></think>';
|
||||
}
|
||||
|
||||
public function hello($name = 'ThinkPHP6')
|
||||
{
|
||||
return 'hello,' . $name;
|
||||
}
|
||||
}
|
||||
133
thinkphp/app/controller/api/v1/Chuku.php
Normal file
133
thinkphp/app/controller/api/v1/Chuku.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller\api\v1;
|
||||
|
||||
use app\service\DataIsolation;
|
||||
use app\service\MssqlQuery;
|
||||
use app\model\ApiLog;
|
||||
use think\facade\Log;
|
||||
use think\Request;
|
||||
|
||||
class Chuku
|
||||
{
|
||||
/**
|
||||
* 销售数据拉取接口
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
$authContext = $request->authContext;
|
||||
$permission = $authContext['permission'];
|
||||
|
||||
// 1. 数据隔离
|
||||
try {
|
||||
$supplierName = $request->param('supplier_name', '');
|
||||
$isolation = DataIsolation::buildIsolationClause($permission, 'chuku', $supplierName);
|
||||
} catch (\Exception $e) {
|
||||
$durationMs = (int)((microtime(true) - $startTime) * 1000);
|
||||
$logEntry = new ApiLog();
|
||||
$logEntry->api_key_id = $authContext['api_key_id'];
|
||||
$logEntry->client_id = $authContext['client_id'];
|
||||
$logEntry->endpoint = '/api/v1/chuku';
|
||||
$logEntry->request_params = $request->param();
|
||||
$logEntry->response_code = 403;
|
||||
$logEntry->response_body = ['code' => 403, 'msg' => $e->getMessage()];
|
||||
$logEntry->record_count = 0;
|
||||
$logEntry->duration_ms = $durationMs;
|
||||
$logEntry->ip_address = $request->ip();
|
||||
$logEntry->error_message = substr($e->getMessage(), 0, 500);
|
||||
$logEntry->save();
|
||||
|
||||
return json([
|
||||
'code' => 403,
|
||||
'msg' => $e->getMessage()
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 2. 获取请求参数
|
||||
$startDate = $request->param('start_date');
|
||||
$endDate = $request->param('end_date');
|
||||
$page = (int)$request->param('page', 1);
|
||||
$limit = min((int)$request->param('limit', 50), env('rate_limit.max_page_size', 500));
|
||||
|
||||
// 3. 查询数据
|
||||
$errorMsg = null;
|
||||
$sqlQuery = '';
|
||||
try {
|
||||
$result = MssqlQuery::queryChuku(
|
||||
$startDate,
|
||||
$endDate,
|
||||
$page,
|
||||
$limit,
|
||||
$isolation['sql'],
|
||||
$isolation['params']
|
||||
);
|
||||
|
||||
$records = $result['records'];
|
||||
$total = $result['total'];
|
||||
$sqlQuery = $result['sql'];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('销售查询失败: ' . $e->getMessage());
|
||||
$errorMsg = $e->getMessage();
|
||||
$durationMs = (int)((microtime(true) - $startTime) * 1000);
|
||||
$logEntry = new ApiLog();
|
||||
$logEntry->api_key_id = $authContext['api_key_id'];
|
||||
$logEntry->client_id = $authContext['client_id'];
|
||||
$logEntry->endpoint = '/api/v1/chuku';
|
||||
$logEntry->request_params = [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'supplier_name' => $supplierName,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
];
|
||||
$logEntry->response_code = 500;
|
||||
$logEntry->response_body = ['code' => 500, 'msg' => '数据查询失败: ' . $e->getMessage()];
|
||||
$logEntry->record_count = 0;
|
||||
$logEntry->sql_query = $sqlQuery;
|
||||
$logEntry->duration_ms = $durationMs;
|
||||
$logEntry->ip_address = $request->ip();
|
||||
$logEntry->error_message = substr($e->getMessage(), 0, 500);
|
||||
$logEntry->save();
|
||||
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '数据查询失败: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
|
||||
// 4. 记录日志
|
||||
$durationMs = (int)((microtime(true) - $startTime) * 1000);
|
||||
|
||||
$logEntry = new ApiLog();
|
||||
$logEntry->api_key_id = $authContext['api_key_id'];
|
||||
$logEntry->client_id = $authContext['client_id'];
|
||||
$logEntry->endpoint = '/api/v1/chuku';
|
||||
$logEntry->request_params = [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'supplier_name' => $supplierName,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
];
|
||||
$logEntry->response_code = 200;
|
||||
$logEntry->response_body = [
|
||||
'total' => $total,
|
||||
'count' => count($records)
|
||||
];
|
||||
$logEntry->record_count = count($records);
|
||||
$logEntry->sql_query = $sqlQuery;
|
||||
$logEntry->duration_ms = $durationMs;
|
||||
$logEntry->ip_address = $request->ip();
|
||||
$logEntry->error_message = $errorMsg ? substr($errorMsg, 0, 500) : null;
|
||||
$logEntry->save();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'total' => $total,
|
||||
'data' => $records
|
||||
]);
|
||||
}
|
||||
}
|
||||
133
thinkphp/app/controller/api/v1/Ruku.php
Normal file
133
thinkphp/app/controller/api/v1/Ruku.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller\api\v1;
|
||||
|
||||
use app\service\DataIsolation;
|
||||
use app\service\MssqlQuery;
|
||||
use app\model\ApiLog;
|
||||
use think\facade\Log;
|
||||
use think\Request;
|
||||
|
||||
class Ruku
|
||||
{
|
||||
/**
|
||||
* 入库数据拉取接口
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
$authContext = $request->authContext;
|
||||
$permission = $authContext['permission'];
|
||||
|
||||
// 1. 数据隔离
|
||||
try {
|
||||
$supplierName = $request->param('supplier_name', '');
|
||||
$isolation = DataIsolation::buildIsolationClause($permission, 'ruku', $supplierName);
|
||||
} catch (\Exception $e) {
|
||||
$durationMs = (int)((microtime(true) - $startTime) * 1000);
|
||||
$logEntry = new ApiLog();
|
||||
$logEntry->api_key_id = $authContext['api_key_id'];
|
||||
$logEntry->client_id = $authContext['client_id'];
|
||||
$logEntry->endpoint = '/api/v1/ruku';
|
||||
$logEntry->request_params = $request->param();
|
||||
$logEntry->response_code = 403;
|
||||
$logEntry->response_body = ['code' => 403, 'msg' => $e->getMessage()];
|
||||
$logEntry->record_count = 0;
|
||||
$logEntry->duration_ms = $durationMs;
|
||||
$logEntry->ip_address = $request->ip();
|
||||
$logEntry->error_message = substr($e->getMessage(), 0, 500);
|
||||
$logEntry->save();
|
||||
|
||||
return json([
|
||||
'code' => 403,
|
||||
'msg' => $e->getMessage()
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 2. 获取请求参数
|
||||
$startDate = $request->param('start_date');
|
||||
$endDate = $request->param('end_date');
|
||||
$page = (int)$request->param('page', 1);
|
||||
$limit = min((int)$request->param('limit', 50), env('rate_limit.max_page_size', 500));
|
||||
|
||||
// 3. 查询数据
|
||||
$errorMsg = null;
|
||||
$sqlQuery = '';
|
||||
try {
|
||||
$result = MssqlQuery::queryRuku(
|
||||
$startDate,
|
||||
$endDate,
|
||||
$page,
|
||||
$limit,
|
||||
$isolation['sql'],
|
||||
$isolation['params']
|
||||
);
|
||||
|
||||
$records = $result['records'];
|
||||
$total = $result['total'];
|
||||
$sqlQuery = $result['sql'];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('入库查询失败: ' . $e->getMessage());
|
||||
$errorMsg = $e->getMessage();
|
||||
$durationMs = (int)((microtime(true) - $startTime) * 1000);
|
||||
$logEntry = new ApiLog();
|
||||
$logEntry->api_key_id = $authContext['api_key_id'];
|
||||
$logEntry->client_id = $authContext['client_id'];
|
||||
$logEntry->endpoint = '/api/v1/ruku';
|
||||
$logEntry->request_params = [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'supplier_name' => $supplierName,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
];
|
||||
$logEntry->response_code = 500;
|
||||
$logEntry->response_body = ['code' => 500, 'msg' => '数据查询失败: ' . $e->getMessage()];
|
||||
$logEntry->record_count = 0;
|
||||
$logEntry->sql_query = $sqlQuery;
|
||||
$logEntry->duration_ms = $durationMs;
|
||||
$logEntry->ip_address = $request->ip();
|
||||
$logEntry->error_message = substr($e->getMessage(), 0, 500);
|
||||
$logEntry->save();
|
||||
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '数据查询失败: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
|
||||
// 4. 记录日志
|
||||
$durationMs = (int)((microtime(true) - $startTime) * 1000);
|
||||
|
||||
$logEntry = new ApiLog();
|
||||
$logEntry->api_key_id = $authContext['api_key_id'];
|
||||
$logEntry->client_id = $authContext['client_id'];
|
||||
$logEntry->endpoint = '/api/v1/ruku';
|
||||
$logEntry->request_params = [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'supplier_name' => $supplierName,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
];
|
||||
$logEntry->response_code = 200;
|
||||
$logEntry->response_body = [
|
||||
'total' => $total,
|
||||
'count' => count($records)
|
||||
];
|
||||
$logEntry->record_count = count($records);
|
||||
$logEntry->sql_query = $sqlQuery;
|
||||
$logEntry->duration_ms = $durationMs;
|
||||
$logEntry->ip_address = $request->ip();
|
||||
$logEntry->error_message = $errorMsg ? substr($errorMsg, 0, 500) : null;
|
||||
$logEntry->save();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'total' => $total,
|
||||
'data' => $records
|
||||
]);
|
||||
}
|
||||
}
|
||||
17
thinkphp/app/event.php
Normal file
17
thinkphp/app/event.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
// 事件定义文件
|
||||
return [
|
||||
'bind' => [
|
||||
],
|
||||
|
||||
'listen' => [
|
||||
'AppInit' => [],
|
||||
'HttpRun' => [],
|
||||
'HttpEnd' => [],
|
||||
'LogLevel' => [],
|
||||
'LogWrite' => [],
|
||||
],
|
||||
|
||||
'subscribe' => [
|
||||
],
|
||||
];
|
||||
12
thinkphp/app/middleware.php
Normal file
12
thinkphp/app/middleware.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
// 全局中间件定义文件
|
||||
return [
|
||||
// 全局请求缓存
|
||||
// \think\middleware\CheckRequestCache::class,
|
||||
// 多语言加载
|
||||
// \think\middleware\LoadLangPack::class,
|
||||
// Session初始化
|
||||
// \think\middleware\SessionInit::class,
|
||||
// 跨域处理
|
||||
\app\middleware\CrossDomain::class,
|
||||
];
|
||||
55
thinkphp/app/middleware/AdminAuth.php
Normal file
55
thinkphp/app/middleware/AdminAuth.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace app\middleware;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use think\Request;
|
||||
use think\Response;
|
||||
|
||||
/**
|
||||
* 管理员认证中间件
|
||||
*/
|
||||
class AdminAuth
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
*/
|
||||
public function handle(Request $request, \Closure $next)
|
||||
{
|
||||
$token = $request->header('Authorization');
|
||||
|
||||
if (empty($token)) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '未提供认证令牌'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 移除 "Bearer " 前缀
|
||||
if (strpos($token, 'Bearer ') === 0) {
|
||||
$token = substr($token, 7);
|
||||
}
|
||||
|
||||
try {
|
||||
$secretKey = env('jwt.secret_key', 'your-super-secret-key-change-in-production-32chars!');
|
||||
$algorithm = env('jwt.algorithm', 'HS256');
|
||||
|
||||
$decoded = JWT::decode($token, new Key($secretKey, $algorithm));
|
||||
$payload = (array)$decoded;
|
||||
|
||||
// 将管理员信息存入请求
|
||||
$request->adminUser = [
|
||||
'id' => $payload['sub'],
|
||||
'username' => $payload['username'],
|
||||
];
|
||||
|
||||
return $next($request);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '无效的认证令牌'
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
}
|
||||
92
thinkphp/app/middleware/Auth.php
Normal file
92
thinkphp/app/middleware/Auth.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace app\middleware;
|
||||
|
||||
use app\model\ApiKey;
|
||||
use app\model\Client;
|
||||
use app\model\Permission;
|
||||
use app\service\Signature;
|
||||
use think\Request;
|
||||
use think\Response;
|
||||
|
||||
/**
|
||||
* API鉴权中间件
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
*/
|
||||
public function handle(Request $request, \Closure $next)
|
||||
{
|
||||
// 1. 提取必要Headers
|
||||
$apiKey = $request->header('X-API-Key');
|
||||
$timestamp = $request->header('X-Timestamp');
|
||||
$sign = $request->header('X-Sign');
|
||||
|
||||
if (empty($apiKey) || empty($timestamp) || empty($sign)) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '缺少鉴权头: 需要 X-API-Key, X-Timestamp, X-Sign'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 2. 查询API Key(连带查询client和permission)
|
||||
$apiKeyObj = ApiKey::with(['client', 'permission'])
|
||||
->where('api_key', $apiKey)
|
||||
->find();
|
||||
|
||||
if (!$apiKeyObj) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '无效的 API Key'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 3. 检查状态
|
||||
if ($apiKeyObj->status != ApiKey::STATUS_ENABLED) {
|
||||
return json([
|
||||
'code' => 403,
|
||||
'msg' => 'API Key 已被禁用'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$clientObj = $apiKeyObj->client;
|
||||
if (!$clientObj || $clientObj->status != Client::STATUS_ENABLED) {
|
||||
return json([
|
||||
'code' => 403,
|
||||
'msg' => '客户账号已被停用,API 访问被拒绝'
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 4. 验证签名
|
||||
$params = $request->param();
|
||||
if (!Signature::verify($params, $timestamp, $sign, $apiKeyObj->api_secret_hash)) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '签名验证失败'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 5. 检查权限配置
|
||||
$permission = Permission::where('api_key_id', $apiKeyObj->id)->find();
|
||||
if (!$permission) {
|
||||
return json([
|
||||
'code' => 403,
|
||||
'msg' => '未配置 API 权限,请联系管理员'
|
||||
], 403);
|
||||
}
|
||||
// 6. 将鉴权上下文存入请求
|
||||
$request->authContext = [
|
||||
'api_key_id' => $apiKeyObj->id,
|
||||
'api_key' => $apiKeyObj->api_key,
|
||||
'client_id' => $clientObj->id,
|
||||
'client_name' => $clientObj->name,
|
||||
'permission' => $permission,
|
||||
'rate_limit_per_minute' => $apiKeyObj->rate_limit_per_minute,
|
||||
'rate_limit_per_day' => $apiKeyObj->rate_limit_per_day,
|
||||
];
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
24
thinkphp/app/middleware/CrossDomain.php
Normal file
24
thinkphp/app/middleware/CrossDomain.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace app\middleware;
|
||||
|
||||
class CrossDomain
|
||||
{
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
$header = [
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Methods' => 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type,Authorization,Token,X-Requested-With,X-API-Key,X-Timestamp,X-Sign',
|
||||
'Access-Control-Max-Age' => 1800,
|
||||
];
|
||||
|
||||
if ($request->method() == 'OPTIONS') {
|
||||
return response('', 204)->header($header);
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
$response->header($header);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
67
thinkphp/app/middleware/RateLimit.php
Normal file
67
thinkphp/app/middleware/RateLimit.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace app\middleware;
|
||||
|
||||
use think\facade\Cache;
|
||||
use think\Request;
|
||||
|
||||
class RateLimit
|
||||
{
|
||||
public function handle(Request $request, \Closure $next)
|
||||
{
|
||||
$authContext = $request->authContext ?? null;
|
||||
if (!$authContext) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$apiKeyId = $authContext['api_key_id'];
|
||||
$limitPerMinute = $authContext['rate_limit_per_minute'] ?? 60;
|
||||
$limitPerDay = $authContext['rate_limit_per_day'] ?? 5000;
|
||||
|
||||
$minuteKey = 'rate_limit:minute:' . $apiKeyId;
|
||||
$dayKey = 'rate_limit:day:' . $apiKeyId . ':' . date('Ymd');
|
||||
|
||||
$minuteCount = $this->getCount($minuteKey);
|
||||
if ($minuteCount >= $limitPerMinute) {
|
||||
return json([
|
||||
'code' => 429,
|
||||
'msg' => '请求过于频繁,请稍后再试'
|
||||
], 429);
|
||||
}
|
||||
|
||||
$dayCount = $this->getCount($dayKey);
|
||||
if ($dayCount >= $limitPerDay) {
|
||||
return json([
|
||||
'code' => 429,
|
||||
'msg' => '今日请求次数已达上限'
|
||||
], 429);
|
||||
}
|
||||
|
||||
$this->increment($minuteKey, 60);
|
||||
$this->increment($dayKey, 86400);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function getCount($key)
|
||||
{
|
||||
try {
|
||||
$value = Cache::get($key);
|
||||
return $value !== false ? (int)$value : 0;
|
||||
} catch (\Exception $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private function increment($key, $ttl)
|
||||
{
|
||||
try {
|
||||
$current = $this->getCount($key);
|
||||
$newValue = $current + 1;
|
||||
Cache::set($key, $newValue, $ttl);
|
||||
return $newValue;
|
||||
} catch (\Exception $e) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
thinkphp/app/model/AdminUser.php
Normal file
33
thinkphp/app/model/AdminUser.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class AdminUser extends Model
|
||||
{
|
||||
protected $name = 'admin_users';
|
||||
protected $pk = 'id';
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'created_at';
|
||||
protected $updateTime = false;
|
||||
|
||||
const STATUS_ENABLED = 1;
|
||||
const STATUS_DISABLED = 0;
|
||||
|
||||
const ROLE_ADMIN = 'admin';
|
||||
const ROLE_VIEWER = 'viewer';
|
||||
|
||||
public function checkPassword($password)
|
||||
{
|
||||
return password_verify($password, $this->getAttr('password_hash') ?? '');
|
||||
}
|
||||
|
||||
public static function getRoles()
|
||||
{
|
||||
return [
|
||||
self::ROLE_ADMIN => '管理员',
|
||||
self::ROLE_VIEWER => '查看员',
|
||||
];
|
||||
}
|
||||
}
|
||||
42
thinkphp/app/model/ApiKey.php
Normal file
42
thinkphp/app/model/ApiKey.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class ApiKey extends Model
|
||||
{
|
||||
protected $name = 'api_keys';
|
||||
protected $pk = 'id';
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'created_at';
|
||||
protected $updateTime = false;
|
||||
|
||||
// 状态枚举
|
||||
const STATUS_ENABLED = 1;
|
||||
const STATUS_DISABLED = 0;
|
||||
|
||||
/**
|
||||
* 关联客户
|
||||
*/
|
||||
public function client()
|
||||
{
|
||||
return $this->belongsTo(Client::class, 'client_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联权限
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
return $this->hasOne(Permission::class, 'api_key_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联日志
|
||||
*/
|
||||
public function logs()
|
||||
{
|
||||
return $this->hasMany(ApiLog::class, 'api_key_id');
|
||||
}
|
||||
}
|
||||
33
thinkphp/app/model/ApiLog.php
Normal file
33
thinkphp/app/model/ApiLog.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class ApiLog extends Model
|
||||
{
|
||||
protected $name = 'api_logs';
|
||||
protected $pk = 'id';
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'created_at';
|
||||
protected $updateTime = false;
|
||||
|
||||
protected $json = ['request_params', 'response_body'];
|
||||
protected $jsonAssoc = true;
|
||||
|
||||
/**
|
||||
* 关联API密钥
|
||||
*/
|
||||
public function apiKey()
|
||||
{
|
||||
return $this->belongsTo(ApiKey::class, 'api_key_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联客户
|
||||
*/
|
||||
public function client()
|
||||
{
|
||||
return $this->belongsTo(Client::class, 'client_id');
|
||||
}
|
||||
}
|
||||
54
thinkphp/app/model/Client.php
Normal file
54
thinkphp/app/model/Client.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class Client extends Model
|
||||
{
|
||||
protected $name = 'clients';
|
||||
protected $pk = 'id';
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'created_at';
|
||||
protected $updateTime = 'updated_at';
|
||||
protected $append = ['api_keys'];
|
||||
|
||||
// 客户类型枚举
|
||||
const TYPE_SUPPLIER = 'supplier';
|
||||
const TYPE_DISTRIBUTOR = 'distributor';
|
||||
const TYPE_HOSPITAL = 'hospital';
|
||||
const TYPE_OTHER = 'other';
|
||||
|
||||
// 状态枚举
|
||||
const STATUS_ENABLED = 1;
|
||||
const STATUS_DISABLED = 0;
|
||||
|
||||
/**
|
||||
* 关联API密钥
|
||||
*/
|
||||
public function apiKeys()
|
||||
{
|
||||
return $this->hasMany(ApiKey::class, 'client_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取api_keys字段(下划线命名)
|
||||
*/
|
||||
public function getApiKeysAttr()
|
||||
{
|
||||
return $this->apiKeys()->select()->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户类型列表
|
||||
*/
|
||||
public static function getClientTypes()
|
||||
{
|
||||
return [
|
||||
self::TYPE_SUPPLIER => '供应商',
|
||||
self::TYPE_DISTRIBUTOR => '分销商',
|
||||
self::TYPE_HOSPITAL => '医院',
|
||||
self::TYPE_OTHER => '其他',
|
||||
];
|
||||
}
|
||||
}
|
||||
55
thinkphp/app/model/Permission.php
Normal file
55
thinkphp/app/model/Permission.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class Permission extends Model
|
||||
{
|
||||
protected $name = 'permissions';
|
||||
protected $pk = 'id';
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'created_at';
|
||||
protected $updateTime = 'updated_at';
|
||||
|
||||
// 权限枚举
|
||||
const ALLOW_NO = 0;
|
||||
const ALLOW_YES = 1;
|
||||
|
||||
// 数据模式枚举
|
||||
const MODE_FILTER = 'filter';
|
||||
const MODE_CUSTOM = 'custom';
|
||||
const MODE_FULL = 'full';
|
||||
|
||||
/**
|
||||
* 关联API密钥
|
||||
*/
|
||||
public function apiKey()
|
||||
{
|
||||
return $this->belongsTo(ApiKey::class, 'api_key_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否允许访问接口
|
||||
*/
|
||||
public function canAccess($endpoint)
|
||||
{
|
||||
if ($this->isFullData()) {
|
||||
return true;
|
||||
}
|
||||
if ($endpoint === 'chuku') {
|
||||
return $this->allow_chuku == self::ALLOW_YES;
|
||||
} elseif ($endpoint === 'ruku') {
|
||||
return $this->allow_ruku == self::ALLOW_YES;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否允许全量数据
|
||||
*/
|
||||
public function isFullData()
|
||||
{
|
||||
return $this->allow_full_data == self::ALLOW_YES;
|
||||
}
|
||||
}
|
||||
9
thinkphp/app/provider.php
Normal file
9
thinkphp/app/provider.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
use app\ExceptionHandle;
|
||||
use app\Request;
|
||||
|
||||
// 容器Provider定义文件
|
||||
return [
|
||||
'think\Request' => Request::class,
|
||||
'think\exception\Handle' => ExceptionHandle::class,
|
||||
];
|
||||
9
thinkphp/app/service.php
Normal file
9
thinkphp/app/service.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use app\AppService;
|
||||
|
||||
// 系统服务定义文件
|
||||
// 服务在完成全局初始化之后执行
|
||||
return [
|
||||
AppService::class,
|
||||
];
|
||||
169
thinkphp/app/service/DataIsolation.php
Normal file
169
thinkphp/app/service/DataIsolation.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use app\model\Permission;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 数据隔离服务
|
||||
*/
|
||||
class DataIsolation
|
||||
{
|
||||
/**
|
||||
* 构建数据隔离WHERE子句
|
||||
*
|
||||
* @param Permission $permission 权限记录
|
||||
* @param string $endpoint 接口标识 chuku/ruku
|
||||
* @param string|null $customerName 客户名称(全量数据权限时可选)
|
||||
* @return array ['sql' => string, 'params' => array]
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function buildIsolationClause($permission, $endpoint, $customerName = null)
|
||||
{
|
||||
// 1. 检查接口权限
|
||||
if ($endpoint === 'chuku' && !$permission->canAccess('chuku')) {
|
||||
throw new \Exception('该 API Key 未被授权访问销售数据接口');
|
||||
}
|
||||
if ($endpoint === 'ruku' && !$permission->canAccess('ruku')) {
|
||||
throw new \Exception('该 API Key 未被授权访问入库数据接口');
|
||||
}
|
||||
|
||||
// 2. 全量数据权限:检查是否传入供方名称过滤
|
||||
if ($permission->isFullData()) {
|
||||
Log::debug("API Key {$permission->api_key_id} 拥有全量数据权限");
|
||||
|
||||
if (!empty($customerName)) {
|
||||
$filterField = 'k.[供方全称]';
|
||||
$sqlFragment = " AND {$filterField} LIKE :supplier_name";
|
||||
$params = ['supplier_name' => '%' . $customerName . '%'];
|
||||
Log::debug("全量数据模式下按供方名称过滤: {$filterField} LIKE %{$customerName}%");
|
||||
return ['sql' => $sqlFragment, 'params' => $params];
|
||||
}
|
||||
|
||||
return ['sql' => '', 'params' => []];
|
||||
}
|
||||
|
||||
// 3. 根据数据模式确定过滤方式
|
||||
$dataMode = $permission->data_mode;
|
||||
|
||||
if ($dataMode === Permission::MODE_FULL) {
|
||||
return ['sql' => '', 'params' => []];
|
||||
}
|
||||
|
||||
// 4. 使用数据库中已配置的过滤规则
|
||||
if (!empty($permission->filter_field) && !empty($permission->filter_values)) {
|
||||
return self::buildCustomFilter($permission, $endpoint);
|
||||
}
|
||||
|
||||
// 5. 自动过滤:根据接口类型自动选择过滤字段和值
|
||||
$apiKey = $permission->apiKey;
|
||||
$client = $apiKey->client;
|
||||
|
||||
if (!$client) {
|
||||
throw new \Exception('API Key 未关联客户');
|
||||
}
|
||||
|
||||
return self::buildAutoFilter($permission, $endpoint, $client);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建自定义过滤规则
|
||||
*/
|
||||
private static function buildCustomFilter($permission, $endpoint)
|
||||
{
|
||||
$filterField = $permission->filter_field;
|
||||
$filterValues = $permission->filter_values;
|
||||
|
||||
if (empty($filterField) || empty($filterValues)) {
|
||||
Log::warning("API Key {$permission->api_key_id} 使用自定义模式,但未配置过滤规则,拒绝访问");
|
||||
throw new \Exception('权限配置不完整:需要手动配置过滤字段和过滤值');
|
||||
}
|
||||
|
||||
if (!is_array($filterValues)) {
|
||||
$filterValues = json_decode($filterValues, true);
|
||||
}
|
||||
|
||||
if ($filterValues === null || !is_array($filterValues) || empty($filterValues)) {
|
||||
Log::warning("API Key {$permission->api_key_id} 过滤值为空或无效");
|
||||
throw new \Exception('过滤值列表为空或无效,请联系管理员');
|
||||
}
|
||||
|
||||
if (!self::validateFilterField($filterField, $endpoint)) {
|
||||
throw new \Exception('非法的过滤字段');
|
||||
}
|
||||
|
||||
$prefixedField = self::prefixFilterField($filterField, $endpoint);
|
||||
|
||||
$placeholders = [];
|
||||
$params = [];
|
||||
foreach ($filterValues as $index => $value) {
|
||||
$placeholder = ':iso_' . $index;
|
||||
$placeholders[] = $placeholder;
|
||||
$params['iso_' . $index] = $value;
|
||||
}
|
||||
|
||||
$sqlFragment = " AND {$prefixedField} IN (" . implode(',', $placeholders) . ')';
|
||||
Log::debug("数据隔离: {$prefixedField} IN " . json_encode($filterValues));
|
||||
|
||||
return ['sql' => $sqlFragment, 'params' => $params];
|
||||
}
|
||||
|
||||
/**
|
||||
* 为过滤字段添加表别名前缀
|
||||
*/
|
||||
private static function prefixFilterField($field, $endpoint)
|
||||
{
|
||||
if (strpos($field, '[') === 0) {
|
||||
$field = substr($field, 1, -1);
|
||||
}
|
||||
|
||||
$prefixMap = [
|
||||
'供方全称' => 'k.[供方全称]',
|
||||
'供方序号' => 'k.[供方序号]',
|
||||
'产品名称' => 'p.[产品名称]',
|
||||
'产品序号' => 'p.[产品序号]',
|
||||
];
|
||||
|
||||
if ($endpoint === 'chuku') {
|
||||
$prefixMap['销方全称'] = 'f.[销方全称]';
|
||||
$prefixMap['销方序号'] = 'f.[销方序号]';
|
||||
}
|
||||
|
||||
return $prefixMap[$field] ?? "[{$field}]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建自动过滤规则
|
||||
*/
|
||||
private static function buildAutoFilter($permission, $endpoint, $client)
|
||||
{
|
||||
$filterField = 'k.[供方全称]';
|
||||
|
||||
$filterValue = $client->name;
|
||||
$sqlFragment = " AND {$filterField} = :iso_0";
|
||||
$params = ['iso_0' => $filterValue];
|
||||
|
||||
Log::debug("数据隔离: {$filterField} = {$filterValue}");
|
||||
|
||||
return ['sql' => $sqlFragment, 'params' => $params];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证过滤字段是否在白名单内(防注入)
|
||||
*/
|
||||
private static function validateFilterField($field, $endpoint)
|
||||
{
|
||||
$allowedFields = [
|
||||
'供方全称', '供方序号',
|
||||
'产品名称', '产品序号',
|
||||
'customer_name', 'supplier_name', 'customer_code', 'supplier_code',
|
||||
];
|
||||
|
||||
if ($endpoint === 'chuku') {
|
||||
$allowedFields = array_merge($allowedFields, ['销方全称', '销方序号']);
|
||||
}
|
||||
|
||||
return in_array($field, $allowedFields);
|
||||
}
|
||||
}
|
||||
428
thinkphp/app/service/MssqlQuery.php
Normal file
428
thinkphp/app/service/MssqlQuery.php
Normal file
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
class MssqlQuery
|
||||
{
|
||||
private static $healthCache = [
|
||||
'available' => null,
|
||||
'checked_at' => 0
|
||||
];
|
||||
private static $healthCheckInterval = 60;
|
||||
|
||||
private static function isMssqlAvailable()
|
||||
{
|
||||
$now = time();
|
||||
if ($now - self::$healthCache['checked_at'] < self::$healthCheckInterval) {
|
||||
return self::$healthCache['available'];
|
||||
}
|
||||
|
||||
try {
|
||||
Db::connect('mssql')->query('SELECT 1');
|
||||
self::$healthCache['available'] = true;
|
||||
self::$healthCache['checked_at'] = $now;
|
||||
Log::info('SQL Server 连接恢复正常');
|
||||
} catch (\Exception $e) {
|
||||
self::$healthCache['available'] = false;
|
||||
self::$healthCache['checked_at'] = $now;
|
||||
Log::warning("SQL Server 不可用(下次探测 " . self::$healthCheckInterval . "s 后): " . $e->getMessage());
|
||||
}
|
||||
|
||||
return self::$healthCache['available'];
|
||||
}
|
||||
|
||||
public static function queryChuku($startDate, $endDate, $page, $limit, $isolationSql, $isolationParams)
|
||||
{
|
||||
if (!self::isMssqlAvailable()) {
|
||||
throw new \Exception('SQL Server 连接不可用');
|
||||
}
|
||||
|
||||
return self::queryChukuReal($startDate, $endDate, $page, $limit, $isolationSql, $isolationParams);
|
||||
}
|
||||
|
||||
private static function queryChukuReal($startDate, $endDate, $page, $limit, $isolationSql, $isolationParams)
|
||||
{
|
||||
$dateCondition = '';
|
||||
$dateParams = [];
|
||||
|
||||
if ($startDate) {
|
||||
$dateCondition .= " AND x.[日期] >= :start_date";
|
||||
$dateParams['start_date'] = $startDate;
|
||||
}
|
||||
if ($endDate) {
|
||||
$dateCondition .= " AND x.[日期] <= :end_date";
|
||||
$dateParams['end_date'] = $endDate;
|
||||
}
|
||||
|
||||
$startRow = ($page - 1) * $limit + 1;
|
||||
$endRow = $page * $limit;
|
||||
$countParams = array_merge($dateParams, $isolationParams);
|
||||
$dataParams = array_merge($countParams, [
|
||||
'start_row' => $startRow,
|
||||
'end_row' => $endRow
|
||||
]);
|
||||
|
||||
$sqlResult = self::buildLogSql(self::getChukuBaseSql(), $dateCondition, $isolationSql, $dataParams);
|
||||
$dataSql = $sqlResult['dataSql'];
|
||||
$logSql = $sqlResult['logSql'];
|
||||
|
||||
$countSql = self::getChukuCountSql();
|
||||
$countSql = str_replace(['{date_condition}', '{isolation_condition}'], [$dateCondition, $isolationSql], $countSql);
|
||||
|
||||
Log::debug("出库查询 - 统计SQL: " . $countSql);
|
||||
Log::debug("出库查询 - 统计参数: " . json_encode($countParams));
|
||||
Log::debug("出库查询 - 统计参数数量: " . count($countParams));
|
||||
|
||||
try {
|
||||
$total = Db::connect('mssql')->query($countSql, $countParams);
|
||||
$total = isset($total[0]['total']) ? (int)$total[0]['total'] : 0;
|
||||
Log::debug("出库查询 - 统计成功,总数: " . $total);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("出库查询 - 统计失败: " . $e->getMessage());
|
||||
Log::error("出库查询 - 统计SQL(原始): " . $countSql);
|
||||
Log::error("出库查询 - 统计参数(原始): " . json_encode($countParams));
|
||||
throw $e;
|
||||
}
|
||||
|
||||
Log::debug("出库查询 - 数据SQL: " . $dataSql);
|
||||
Log::debug("出库查询 - 数据参数: " . json_encode($dataParams));
|
||||
Log::debug("出库查询 - 数据参数数量: " . count($dataParams));
|
||||
|
||||
try {
|
||||
$rows = Db::connect('mssql')->query($dataSql, $dataParams);
|
||||
Log::debug("出库查询 - 数据查询成功,行数: " . count($rows));
|
||||
} catch (\Exception $e) {
|
||||
Log::error("出库查询 - 数据查询失败: " . $e->getMessage());
|
||||
Log::error("出库查询 - 数据SQL(原始): " . $dataSql);
|
||||
Log::error("出库查询 - 数据参数(原始): " . json_encode($dataParams));
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$records = [];
|
||||
foreach ($rows as $row) {
|
||||
$records[] = self::convertChukuRow($row);
|
||||
}
|
||||
|
||||
return [
|
||||
'records' => $records,
|
||||
'total' => $total,
|
||||
'sql' => $logSql
|
||||
];
|
||||
}
|
||||
|
||||
public static function queryRuku($startDate, $endDate, $page, $limit, $isolationSql, $isolationParams)
|
||||
{
|
||||
if (!self::isMssqlAvailable()) {
|
||||
throw new \Exception('SQL Server 连接不可用');
|
||||
}
|
||||
|
||||
return self::queryRukuReal($startDate, $endDate, $page, $limit, $isolationSql, $isolationParams);
|
||||
}
|
||||
|
||||
private static function queryRukuReal($startDate, $endDate, $page, $limit, $isolationSql, $isolationParams)
|
||||
{
|
||||
$dateCondition = '';
|
||||
$dateParams = [];
|
||||
|
||||
if ($startDate) {
|
||||
$dateCondition .= " AND x.[日期] >= :start_date";
|
||||
$dateParams['start_date'] = $startDate;
|
||||
}
|
||||
if ($endDate) {
|
||||
$dateCondition .= " AND x.[日期] <= :end_date";
|
||||
$dateParams['end_date'] = $endDate;
|
||||
}
|
||||
|
||||
$startRow = ($page - 1) * $limit + 1;
|
||||
$endRow = $page * $limit;
|
||||
$countParams = array_merge($dateParams, $isolationParams);
|
||||
$dataParams = array_merge($countParams, [
|
||||
'start_row' => $startRow,
|
||||
'end_row' => $endRow
|
||||
]);
|
||||
|
||||
$sqlResult = self::buildLogSql(self::getRukuBaseSql(), $dateCondition, $isolationSql, $dataParams);
|
||||
$dataSql = $sqlResult['dataSql'];
|
||||
$logSql = $sqlResult['logSql'];
|
||||
|
||||
$countSql = self::getRukuCountSql();
|
||||
$countSql = str_replace(['{date_condition}', '{isolation_condition}'], [$dateCondition, $isolationSql], $countSql);
|
||||
|
||||
Log::debug("入库查询 - 统计SQL: " . $countSql);
|
||||
Log::debug("入库查询 - 统计参数: " . json_encode($countParams));
|
||||
|
||||
$total = Db::connect('mssql')->query($countSql, $countParams);
|
||||
$total = isset($total[0]['total']) ? (int)$total[0]['total'] : 0;
|
||||
|
||||
Log::debug("入库查询 - 数据SQL: " . $dataSql);
|
||||
Log::debug("入库查询 - 数据参数: " . json_encode($dataParams));
|
||||
|
||||
$rows = Db::connect('mssql')->query($dataSql, $dataParams);
|
||||
|
||||
$records = [];
|
||||
foreach ($rows as $row) {
|
||||
$records[] = self::convertRukuRow($row);
|
||||
}
|
||||
|
||||
return [
|
||||
'records' => $records,
|
||||
'total' => $total,
|
||||
'sql' => $logSql
|
||||
];
|
||||
}
|
||||
|
||||
private static function buildLogSql($sqlTemplate, $dateCondition, $isolationSql, $allParams)
|
||||
{
|
||||
$dataSql = str_replace(['{date_condition}', '{isolation_condition}'], [$dateCondition, $isolationSql], $sqlTemplate);
|
||||
$logSql = $dataSql;
|
||||
|
||||
foreach ($allParams as $key => $value) {
|
||||
$placeholder = ':' . $key;
|
||||
if (is_string($value)) {
|
||||
$logSql = str_replace($placeholder, "'" . $value . "'", $logSql);
|
||||
} else {
|
||||
$logSql = str_replace($placeholder, (string)$value, $logSql);
|
||||
}
|
||||
}
|
||||
|
||||
return ['dataSql' => $dataSql, 'logSql' => $logSql];
|
||||
}
|
||||
|
||||
private static function convertChukuRow($row)
|
||||
{
|
||||
return [
|
||||
'chuku_id' => $row['chuku_id'] ?? '',
|
||||
'ruku_id' => $row['ruku_id'] ?? '',
|
||||
'ticket_no' => $row['ticket_no'] ?? '',
|
||||
'date' => self::convertDate($row['date'] ?? null),
|
||||
'warehouse' => $row['warehouse'] ?? '',
|
||||
'batch_no' => $row['batch_no'] ?? '',
|
||||
'expire_date' => self::convertDate($row['expire_date'] ?? null),
|
||||
'quantity' => self::convertFloat($row['quantity'] ?? null),
|
||||
'unit_price' => self::convertFloat($row['unit_price'] ?? null),
|
||||
'amount' => self::convertFloat($row['amount'] ?? null),
|
||||
'purchase_price' => self::convertFloat($row['purchase_price'] ?? null),
|
||||
'profit' => self::convertFloat($row['profit'] ?? null),
|
||||
'operator' => $row['operator'] ?? '',
|
||||
'salesman' => $row['salesman'] ?? '',
|
||||
'receiver' => $row['receiver'] ?? '',
|
||||
'summary' => $row['summary'] ?? '',
|
||||
'supplier_id' => $row['supplier_id'] ?? '',
|
||||
'supplier_name' => $row['supplier_name'] ?? '',
|
||||
'supplier_short_name' => $row['supplier_short_name'] ?? '',
|
||||
'customer_id' => $row['customer_id'] ?? '',
|
||||
'customer_name' => $row['customer_name'] ?? '',
|
||||
'customer_short_name' => $row['customer_short_name'] ?? '',
|
||||
'customer_region' => $row['customer_region'] ?? '',
|
||||
'product_id' => $row['product_id'] ?? '',
|
||||
'product_name' => $row['product_name'] ?? '',
|
||||
'product_spec' => $row['product_spec'] ?? '',
|
||||
'product_origin' => $row['product_origin'] ?? '',
|
||||
'product_unit' => $row['product_unit'] ?? '',
|
||||
'manufacturer' => $row['manufacturer'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
private static function convertRukuRow($row)
|
||||
{
|
||||
return [
|
||||
'ruku_id' => $row['ruku_id'] ?? '',
|
||||
'ticket_no' => $row['ticket_no'] ?? '',
|
||||
'date' => self::convertDate($row['date'] ?? null),
|
||||
'warehouse' => $row['warehouse'] ?? '',
|
||||
'batch_no' => $row['batch_no'] ?? '',
|
||||
'expire_date' => self::convertDate($row['expire_date'] ?? null),
|
||||
'quantity' => self::convertFloat($row['quantity'] ?? null),
|
||||
'unit_price' => self::convertFloat($row['unit_price'] ?? null),
|
||||
'amount' => self::convertFloat($row['amount'] ?? null),
|
||||
'purchase_price' => self::convertFloat($row['purchase_price'] ?? null),
|
||||
'warehouse_qty' => self::convertFloat($row['warehouse_qty'] ?? null),
|
||||
'operator' => $row['operator'] ?? '',
|
||||
'purchaser' => $row['purchaser'] ?? '',
|
||||
'responsible_person' => $row['responsible_person'] ?? '',
|
||||
'arrival_no' => $row['arrival_no'] ?? '',
|
||||
'invoice_no' => $row['invoice_no'] ?? '',
|
||||
'summary' => $row['summary'] ?? '',
|
||||
'supplier_id' => $row['supplier_id'] ?? '',
|
||||
'supplier_name' => $row['supplier_name'] ?? '',
|
||||
'supplier_short_name' => $row['supplier_short_name'] ?? '',
|
||||
'product_id' => $row['product_id'] ?? '',
|
||||
'product_name' => $row['product_name'] ?? '',
|
||||
'product_spec' => $row['product_spec'] ?? '',
|
||||
'product_origin' => $row['product_origin'] ?? '',
|
||||
'product_unit' => $row['product_unit'] ?? '',
|
||||
'manufacturer' => $row['manufacturer'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
private static function convertDate($value)
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
try {
|
||||
$base = new \DateTime('1899-12-30');
|
||||
$base->add(new \DateInterval('P' . (int)$value . 'D'));
|
||||
return $base->format('Y-m-d');
|
||||
} catch (\Exception $e) {
|
||||
return (string)$value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($value instanceof \DateTime) {
|
||||
return $value->format('Y-m-d');
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
private static function convertFloat($value)
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return is_numeric($value) ? (float)$value : null;
|
||||
}
|
||||
|
||||
private static function getChukuBaseSql()
|
||||
{
|
||||
return "
|
||||
WITH PageData AS (
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (ORDER BY x.[日期] DESC) AS RowID,
|
||||
x.[出库序号] AS chuku_id,
|
||||
x.[入库序号] AS ruku_id,
|
||||
x.[票号] AS ticket_no,
|
||||
x.[日期] AS date,
|
||||
x.[库房] AS warehouse,
|
||||
x.[批号] AS batch_no,
|
||||
x.[效期] AS expire_date,
|
||||
x.[数量] AS quantity,
|
||||
x.[单价] AS unit_price,
|
||||
x.[金额] AS amount,
|
||||
x.[进价] AS purchase_price,
|
||||
x.[毛利] AS profit,
|
||||
x.[操作员] AS operator,
|
||||
x.[业务员] AS salesman,
|
||||
x.[提货人] AS receiver,
|
||||
x.[摘要] AS summary,
|
||||
x.[供方序号] AS supplier_id,
|
||||
k.[供方全称] AS supplier_name,
|
||||
k.[简称] AS supplier_short_name,
|
||||
x.[销方序号] AS customer_id,
|
||||
f.[销方全称] AS customer_name,
|
||||
f.[简称] AS customer_short_name,
|
||||
f.[地区] AS customer_region,
|
||||
x.[产品序号] AS product_id,
|
||||
p.[产品名称] AS product_name,
|
||||
p.[规格] AS product_spec,
|
||||
p.[产地] AS product_origin,
|
||||
p.[单位] AS product_unit,
|
||||
p.[生产厂家] AS manufacturer
|
||||
FROM [ywsj].[dbo].[vckmx] x
|
||||
JOIN [ywsj].[dbo].[bgfkh] k ON x.[供方序号] = k.[供方序号]
|
||||
JOIN [ywsj].[dbo].[bcp] p ON x.[产品序号] = p.[产品序号]
|
||||
JOIN [ywsj].[dbo].[bxfkh] f ON x.[销方序号] = f.[销方序号]
|
||||
WHERE 1=1
|
||||
{date_condition}
|
||||
{isolation_condition}
|
||||
)
|
||||
SELECT
|
||||
chuku_id, ruku_id, ticket_no, date, warehouse, batch_no, expire_date,
|
||||
quantity, unit_price, amount, purchase_price, profit,
|
||||
operator, salesman, receiver, summary,
|
||||
supplier_id, supplier_name, supplier_short_name,
|
||||
customer_id, customer_name, customer_short_name, customer_region,
|
||||
product_id, product_name, product_spec, product_origin,
|
||||
product_unit, manufacturer
|
||||
FROM PageData
|
||||
WHERE RowID BETWEEN :start_row AND :end_row
|
||||
ORDER BY RowID
|
||||
";
|
||||
}
|
||||
|
||||
private static function getChukuCountSql()
|
||||
{
|
||||
return "
|
||||
SELECT COUNT(*) AS total
|
||||
FROM [ywsj].[dbo].[vckmx] x
|
||||
JOIN [ywsj].[dbo].[bgfkh] k ON x.[供方序号] = k.[供方序号]
|
||||
JOIN [ywsj].[dbo].[bcp] p ON x.[产品序号] = p.[产品序号]
|
||||
JOIN [ywsj].[dbo].[bxfkh] f ON x.[销方序号] = f.[销方序号]
|
||||
WHERE 1=1
|
||||
{date_condition}
|
||||
{isolation_condition}
|
||||
";
|
||||
}
|
||||
|
||||
private static function getRukuBaseSql()
|
||||
{
|
||||
return "
|
||||
WITH PageData AS (
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (ORDER BY x.[日期] DESC) AS RowID,
|
||||
x.[入库序号] AS ruku_id,
|
||||
x.[票号] AS ticket_no,
|
||||
x.[日期] AS date,
|
||||
x.[库房] AS warehouse,
|
||||
x.[批号] AS batch_no,
|
||||
x.[效期] AS expire_date,
|
||||
x.[数量] AS quantity,
|
||||
x.[单价] AS unit_price,
|
||||
x.[金额] AS amount,
|
||||
x.[进价] AS purchase_price,
|
||||
x.[库房数量] AS warehouse_qty,
|
||||
x.[操作员] AS operator,
|
||||
x.[采购员] AS purchaser,
|
||||
x.[负责人] AS responsible_person,
|
||||
x.[来货单号] AS arrival_no,
|
||||
x.[发票单号] AS invoice_no,
|
||||
x.[摘要] AS summary,
|
||||
x.[供方序号] AS supplier_id,
|
||||
k.[供方全称] AS supplier_name,
|
||||
k.[简称] AS supplier_short_name,
|
||||
x.[产品序号] AS product_id,
|
||||
p.[产品名称] AS product_name,
|
||||
p.[规格] AS product_spec,
|
||||
p.[产地] AS product_origin,
|
||||
p.[单位] AS product_unit,
|
||||
p.[生产厂家] AS manufacturer
|
||||
FROM [ywsj].[dbo].[vrkmx] x
|
||||
JOIN [ywsj].[dbo].[bgfkh] k ON x.[供方序号] = k.[供方序号]
|
||||
JOIN [ywsj].[dbo].[bcp] p ON x.[产品序号] = p.[产品序号]
|
||||
WHERE 1=1
|
||||
{date_condition}
|
||||
{isolation_condition}
|
||||
)
|
||||
SELECT
|
||||
ruku_id, ticket_no, date, warehouse, batch_no, expire_date,
|
||||
quantity, unit_price, amount, purchase_price, warehouse_qty,
|
||||
operator, purchaser, responsible_person, arrival_no, invoice_no,
|
||||
summary, supplier_id, supplier_name, supplier_short_name,
|
||||
product_id, product_name, product_spec, product_origin,
|
||||
product_unit, manufacturer
|
||||
FROM PageData
|
||||
WHERE RowID BETWEEN :start_row AND :end_row
|
||||
ORDER BY RowID
|
||||
";
|
||||
}
|
||||
|
||||
private static function getRukuCountSql()
|
||||
{
|
||||
return "
|
||||
SELECT COUNT(*) AS total
|
||||
FROM [ywsj].[dbo].[vrkmx] x
|
||||
JOIN [ywsj].[dbo].[bgfkh] k ON x.[供方序号] = k.[供方序号]
|
||||
JOIN [ywsj].[dbo].[bcp] p ON x.[产品序号] = p.[产品序号]
|
||||
WHERE 1=1
|
||||
{date_condition}
|
||||
{isolation_condition}
|
||||
";
|
||||
}
|
||||
}
|
||||
107
thinkphp/app/service/Signature.php
Normal file
107
thinkphp/app/service/Signature.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
/**
|
||||
* 签名验证服务
|
||||
* 完全按照 Python 版本实现
|
||||
*/
|
||||
class Signature
|
||||
{
|
||||
/**
|
||||
* 生成API Key
|
||||
*/
|
||||
public static function generateApiKey()
|
||||
{
|
||||
return bin2hex(random_bytes(6));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成API Secret
|
||||
*/
|
||||
public static function generateApiSecret()
|
||||
{
|
||||
return bin2hex(random_bytes(10));
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 API Secret 做 SHA256 哈希存储
|
||||
*/
|
||||
public static function hashSecret($secret)
|
||||
{
|
||||
return hash('sha256', $secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证签名(完全按照 Python 版本实现)
|
||||
*
|
||||
* @param array $params 请求查询参数
|
||||
* @param string $timestamp X-Timestamp header 值
|
||||
* @param string $signature X-Sign header 值
|
||||
* @param string $apiSecretHash 数据库存储的 secret 哈希
|
||||
* @return bool
|
||||
*/
|
||||
public static function verify($params, $timestamp, $signature, $apiSecretHash)
|
||||
{
|
||||
try {
|
||||
$ts = (int)$timestamp;
|
||||
if ($ts > 1e12) {
|
||||
$ts = (int)($ts / 1000);
|
||||
}
|
||||
$now = time();
|
||||
$expireSeconds = env('signature.expire_seconds', 300);
|
||||
if (abs($now - $ts) > $expireSeconds) {
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sortedParams = [];
|
||||
foreach ($params as $k => $v) {
|
||||
$sortedParams[$k] = $v;
|
||||
}
|
||||
ksort($sortedParams);
|
||||
|
||||
$paramParts = [];
|
||||
foreach ($sortedParams as $k => $v) {
|
||||
if ($v !== null && $v !== '') {
|
||||
$paramParts[] = "{$k}={$v}";
|
||||
}
|
||||
}
|
||||
$paramStr = implode('&', $paramParts);
|
||||
$fullStr = $paramStr . '×tamp=' . $timestamp;
|
||||
|
||||
$expectedSign = hash_hmac('sha256', $fullStr, $apiSecretHash);
|
||||
|
||||
return strcasecmp($expectedSign, $signature) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名(用于测试)
|
||||
*
|
||||
* @param array $params 请求参数
|
||||
* @param string $timestamp 时间戳字符串
|
||||
* @param string $apiSecret API Secret 明文
|
||||
* @return string
|
||||
*/
|
||||
public static function sign($params, $timestamp, $apiSecret)
|
||||
{
|
||||
$sortedParams = [];
|
||||
foreach ($params as $k => $v) {
|
||||
$sortedParams[$k] = $v;
|
||||
}
|
||||
ksort($sortedParams);
|
||||
|
||||
$paramParts = [];
|
||||
foreach ($sortedParams as $k => $v) {
|
||||
if ($v !== null && $v !== '') {
|
||||
$paramParts[] = "{$k}={$v}";
|
||||
}
|
||||
}
|
||||
$paramStr = implode('&', $paramParts);
|
||||
$fullStr = $paramStr . '×tamp=' . $timestamp;
|
||||
|
||||
return hash_hmac('sha256', $fullStr, $apiSecret);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user