feat: 初始化进销存数据开放平台项目
初始化完整的前后端项目结构,包含ThinkPHP后端API服务和Next.js前端管理系统,添加基础配置、中间件、模型、路由等核心代码,以及项目文档和静态资源。
This commit is contained in:
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