feat: 1. 添加企查查的按钮。新增相关组件和对接接口进行显示。
2. 为51707端口添加只存在交叉评查入口的项目启动配置。入口页添加相关的区分。 3. 完善文档列表的权限功能控制。 4. 隐藏系统概览中高风险用户的统计模块。 fix: 1. 修复合同起草无权访问却生成了新的模板文件的问题。 2. 修复文档类型无法编辑入口模块的问题。
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 服务端路由权限检查工具
|
||||
* 用于在 action 中检查用户是否有权限访问目标路由
|
||||
*/
|
||||
|
||||
import { getUserRoutesByRole, type MenuItem } from './user-routes';
|
||||
|
||||
/**
|
||||
* 从 MenuItem 数组中提取所有路径(包括子路由)
|
||||
*/
|
||||
function extractAllPaths(menuItems: MenuItem[]): string[] {
|
||||
const paths: string[] = [];
|
||||
|
||||
function traverse(items: MenuItem[]) {
|
||||
for (const item of items) {
|
||||
paths.push(item.path);
|
||||
if (item.children && item.children.length > 0) {
|
||||
traverse(item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(menuItems);
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径段是否看起来像动态ID
|
||||
*/
|
||||
function isDynamicIdSegment(segment: string): boolean {
|
||||
// 纯数字
|
||||
if (/^\d+$/.test(segment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// UUID格式
|
||||
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 包含数字的混合ID
|
||||
if (/\d/.test(segment) && !/^[a-z]+(-[a-z]+)*$/i.test(segment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否在允许列表中
|
||||
*/
|
||||
function isPathAllowed(pathname: string, allowedPaths: string[]): boolean {
|
||||
// 精确匹配
|
||||
if (allowedPaths.includes(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 动态路由匹配
|
||||
for (const allowedPath of allowedPaths) {
|
||||
if (pathname.startsWith(allowedPath + '/')) {
|
||||
const subPath = pathname.substring(allowedPath.length + 1);
|
||||
const segments = subPath.split('/');
|
||||
const firstSegment = segments[0];
|
||||
|
||||
if (isDynamicIdSegment(firstSegment)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根路径
|
||||
if (pathname === '/') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface CheckRoutePermissionResult {
|
||||
/** 是否有权限访问 */
|
||||
allowed: boolean;
|
||||
/** 错误信息(当 allowed 为 false 时) */
|
||||
error?: string;
|
||||
/** 用户允许访问的所有路由 */
|
||||
allowedPaths?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限访问指定路由
|
||||
*
|
||||
* @param targetPath 目标路由路径(如 '/contract-draft/1')
|
||||
* @param userRole 用户角色
|
||||
* @param jwt JWT token
|
||||
* @returns 权限检查结果
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 在 action 中使用
|
||||
* export async function action({ request }: ActionFunctionArgs) {
|
||||
* const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
*
|
||||
* // 检查用户是否有权限访问目标路由
|
||||
* const permissionCheck = await checkRoutePermission(
|
||||
* '/contract-draft/1',
|
||||
* userInfo.role,
|
||||
* frontendJWT
|
||||
* );
|
||||
*
|
||||
* if (!permissionCheck.allowed) {
|
||||
* return Response.json({ error: permissionCheck.error }, { status: 403 });
|
||||
* }
|
||||
*
|
||||
* // 继续执行 action 逻辑...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function checkRoutePermission(
|
||||
targetPath: string,
|
||||
userRole: string,
|
||||
jwt?: string
|
||||
): Promise<CheckRoutePermissionResult> {
|
||||
if (!jwt) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: '未提供认证信息'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取用户的路由权限(包含隐藏路由,用于权限校验)
|
||||
const routesResult = await getUserRoutesByRole(userRole, jwt, true);
|
||||
|
||||
if (!routesResult.success || !routesResult.data) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: routesResult.error || '获取用户权限失败'
|
||||
};
|
||||
}
|
||||
|
||||
// 提取所有允许的路径
|
||||
const allowedPaths = extractAllPaths(routesResult.data);
|
||||
|
||||
// 检查目标路径是否在允许列表中
|
||||
const allowed = isPathAllowed(targetPath, allowedPaths);
|
||||
|
||||
if (!allowed) {
|
||||
console.warn(`[checkRoutePermission] 用户无权访问路由: ${targetPath}`);
|
||||
console.warn(`[checkRoutePermission] 用户允许的路由: ${allowedPaths.join(', ')}`);
|
||||
}
|
||||
|
||||
return {
|
||||
allowed,
|
||||
error: allowed ? undefined : '您没有权限访问目标页面',
|
||||
allowedPaths
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[checkRoutePermission] 权限检查失败:', error);
|
||||
return {
|
||||
allowed: false,
|
||||
error: '权限检查失败,请稍后重试'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化版:直接检查是否有权限,抛出 403 Response
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* export async function action({ request }: ActionFunctionArgs) {
|
||||
* const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
*
|
||||
* // 如果没有权限,会自动抛出 403 Response
|
||||
* await requireRoutePermission('/contract-draft/1', userInfo.role, frontendJWT);
|
||||
*
|
||||
* // 继续执行 action 逻辑...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function requireRoutePermission(
|
||||
targetPath: string,
|
||||
userRole: string,
|
||||
jwt?: string
|
||||
): Promise<void> {
|
||||
const result = await checkRoutePermission(targetPath, userRole, jwt);
|
||||
|
||||
if (!result.allowed) {
|
||||
throw new Response(result.error || '无权访问', { status: 403 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* 企查查 API 接口封装
|
||||
* 提供企业工商信息和失信核查查询功能
|
||||
*/
|
||||
|
||||
import { post } from '../axios-client';
|
||||
import type {
|
||||
BusinessInfoResult,
|
||||
DishonestyResult,
|
||||
Paging,
|
||||
} from '~/components/corporate-information/types';
|
||||
|
||||
// ==================== 请求/响应类型定义 ====================
|
||||
|
||||
/** 查询企业信息请求参数 */
|
||||
export interface QueryCompanyRequest {
|
||||
/** 查询关键词(企业名称或统一社会信用代码),2-200字符 */
|
||||
keyword: string;
|
||||
/** 是否强制刷新,默认false */
|
||||
force_refresh?: boolean;
|
||||
}
|
||||
|
||||
/** 查询企业信息响应数据 */
|
||||
export interface QueryCompanyData {
|
||||
/** 查询关键词 */
|
||||
search_key: string;
|
||||
/** 统一社会信用代码 */
|
||||
credit_code: string;
|
||||
/** 企业名称 */
|
||||
company_name: string;
|
||||
/** 企业工商信息(410接口原始返回) */
|
||||
enterprise: BusinessInfoResult | null;
|
||||
/** 失信核查信息(740接口原始返回) */
|
||||
dishonesty: {
|
||||
/** 核查结果:0-无失信记录,1-有失信记录 */
|
||||
VerifyResult: number;
|
||||
/** 失信记录列表 */
|
||||
Data: DishonestyResult['Data'] | null;
|
||||
} | null;
|
||||
/** 是否有失信记录 */
|
||||
has_dishonesty: boolean;
|
||||
/** 失信记录数量 */
|
||||
dishonesty_count: number;
|
||||
/** 数据更新时间 */
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** 查询企业信息响应 */
|
||||
export interface QueryCompanyResponse {
|
||||
/** 查询是否成功 */
|
||||
success: boolean;
|
||||
/** 响应消息 */
|
||||
message: string;
|
||||
/** 企业信息 */
|
||||
data: QueryCompanyData | null;
|
||||
/** 错误码(失败时返回) */
|
||||
error_code?: string;
|
||||
/** 错误详情(失败时返回) */
|
||||
error_details?: {
|
||||
order_number: string;
|
||||
is_charged: boolean;
|
||||
is_valid_request: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Mock 数据 ====================
|
||||
|
||||
/** Mock 企业工商信息(腾讯科技示例) */
|
||||
const MOCK_ENTERPRISE_DATA: BusinessInfoResult = {
|
||||
KeyNo: 'p5ef6e1c39f4817d33dfb4e9',
|
||||
Name: '腾讯科技(深圳)有限公司',
|
||||
No: '440301103206221',
|
||||
BelongOrg: '深圳市市场监督管理局',
|
||||
OperId: 'p5ef6e1c39f4817d33dfb4e9op',
|
||||
OperName: '马化腾',
|
||||
DesignatedRepresentativeList: [],
|
||||
StartDate: '2000-02-24 00:00:00',
|
||||
EndDate: '',
|
||||
Status: '存续',
|
||||
Province: 'GD',
|
||||
UpdatedDate: '2024-12-10 15:30:00',
|
||||
CreditCode: '91440300708461136T',
|
||||
RegistCapi: '65000万人民币',
|
||||
RegisteredCapital: '65000',
|
||||
RegisteredCapitalUnit: '万',
|
||||
RegisteredCapitalCCY: 'CNY',
|
||||
EconKind: '有限责任公司',
|
||||
Address: '深圳市南山区粤海街道科技中一路腾讯大厦35层',
|
||||
Scope: '从事计算机软硬件及周边设备、通讯设备、数码产品、网络设备的技术开发、销售及相关的技术咨询、技术服务;经营进出口业务;从事广告业务;物业管理',
|
||||
TermStart: '2000-02-24 00:00:00',
|
||||
TermEnd: '2050-02-24 00:00:00',
|
||||
CheckDate: '2023-08-15 00:00:00',
|
||||
OrgNo: '708461136',
|
||||
IsOnStock: '1',
|
||||
StockNumber: '00700.HK',
|
||||
StockType: '港股',
|
||||
OriginalName: [
|
||||
{
|
||||
Name: '深圳市腾讯计算机系统有限公司',
|
||||
ChangeDate: '2000-06-01',
|
||||
},
|
||||
],
|
||||
ImageUrl: 'https://img.qichacha.com/xxx.png',
|
||||
EntType: '1',
|
||||
RecCap: '65000万人民币',
|
||||
PaidUpCapital: '65000',
|
||||
PaidUpCapitalUnit: '万',
|
||||
PaidUpCapitalCCY: 'CNY',
|
||||
RevokeInfo: {
|
||||
CancelDate: '',
|
||||
CancelReason: '',
|
||||
RevokeDate: '',
|
||||
RevokeReason: '',
|
||||
},
|
||||
Area: {
|
||||
Province: '广东省',
|
||||
City: '深圳市',
|
||||
County: '南山区',
|
||||
},
|
||||
AreaCode: '440305',
|
||||
};
|
||||
|
||||
/** Mock 失信记录(有失信记录的示例) */
|
||||
const MOCK_DISHONESTY_WITH_RECORDS = {
|
||||
VerifyResult: 1,
|
||||
Data: [
|
||||
{
|
||||
Id: 'c2edc410f8ceed3ed256055baa8df1432',
|
||||
Liandate: '2021-08-10',
|
||||
Anno: '(2021)粤0106执34224号',
|
||||
Executegov: '广州市天河区人民法院',
|
||||
Executestatus: '全部未履行',
|
||||
Publicdate: '2022-01-07',
|
||||
Executeno: '(2020)粤0106民初12345号',
|
||||
ActionRemark: '有履行能力而拒不履行生效法律文书确定义务',
|
||||
Amount: '1098000',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** Mock 无失信记录 */
|
||||
const MOCK_DISHONESTY_NO_RECORDS = {
|
||||
VerifyResult: 0,
|
||||
Data: null,
|
||||
};
|
||||
|
||||
/** 是否使用 Mock 数据 */
|
||||
const USE_MOCK = true;
|
||||
|
||||
// ==================== API 方法 ====================
|
||||
|
||||
/**
|
||||
* 查询企业信息(工商信息 + 失信核查)
|
||||
* @param params 查询参数
|
||||
* @returns 企业完整信息
|
||||
*/
|
||||
export async function queryCompanyInfo(
|
||||
params: QueryCompanyRequest
|
||||
): Promise<QueryCompanyResponse> {
|
||||
// 使用 Mock 数据
|
||||
if (USE_MOCK) {
|
||||
// 模拟网络延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
|
||||
// 根据关键词返回不同的 Mock 数据
|
||||
const hasDishonesty = params.keyword.includes('失信') || params.keyword.includes('建设');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '查询成功',
|
||||
data: {
|
||||
search_key: params.keyword,
|
||||
credit_code: MOCK_ENTERPRISE_DATA.CreditCode,
|
||||
company_name: MOCK_ENTERPRISE_DATA.Name,
|
||||
enterprise: MOCK_ENTERPRISE_DATA,
|
||||
dishonesty: hasDishonesty ? MOCK_DISHONESTY_WITH_RECORDS : MOCK_DISHONESTY_NO_RECORDS,
|
||||
has_dishonesty: hasDishonesty,
|
||||
dishonesty_count: hasDishonesty ? 1 : 0,
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 真实 API 调用
|
||||
const response = await post<QueryCompanyResponse>('/api/v2/qichacha/company', params);
|
||||
|
||||
if (response.error) {
|
||||
return {
|
||||
success: false,
|
||||
message: response.error,
|
||||
data: null,
|
||||
error_code: String(response.status),
|
||||
};
|
||||
}
|
||||
|
||||
return response.data as QueryCompanyResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅查询工商信息
|
||||
* @param params 查询参数
|
||||
* @returns 企业工商信息
|
||||
*/
|
||||
export async function queryEnterpriseInfo(
|
||||
params: QueryCompanyRequest
|
||||
): Promise<QueryCompanyResponse> {
|
||||
if (USE_MOCK) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '查询成功',
|
||||
data: {
|
||||
search_key: params.keyword,
|
||||
credit_code: MOCK_ENTERPRISE_DATA.CreditCode,
|
||||
company_name: MOCK_ENTERPRISE_DATA.Name,
|
||||
enterprise: MOCK_ENTERPRISE_DATA,
|
||||
dishonesty: null,
|
||||
has_dishonesty: false,
|
||||
dishonesty_count: 0,
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const response = await post<QueryCompanyResponse>('/api/v2/qichacha/enterprise', params);
|
||||
|
||||
if (response.error) {
|
||||
return {
|
||||
success: false,
|
||||
message: response.error,
|
||||
data: null,
|
||||
error_code: String(response.status),
|
||||
};
|
||||
}
|
||||
|
||||
return response.data as QueryCompanyResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅查询失信信息
|
||||
* @param params 查询参数
|
||||
* @returns 失信核查信息
|
||||
*/
|
||||
export async function queryDishonestyInfo(
|
||||
params: QueryCompanyRequest
|
||||
): Promise<QueryCompanyResponse> {
|
||||
if (USE_MOCK) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const hasDishonesty = params.keyword.includes('失信') || params.keyword.includes('建设');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '查询成功',
|
||||
data: {
|
||||
search_key: params.keyword,
|
||||
credit_code: '',
|
||||
company_name: params.keyword,
|
||||
enterprise: null,
|
||||
dishonesty: hasDishonesty ? MOCK_DISHONESTY_WITH_RECORDS : MOCK_DISHONESTY_NO_RECORDS,
|
||||
has_dishonesty: hasDishonesty,
|
||||
dishonesty_count: hasDishonesty ? 1 : 0,
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const response = await post<QueryCompanyResponse>('/api/v2/qichacha/dishonesty', params);
|
||||
|
||||
if (response.error) {
|
||||
return {
|
||||
success: false,
|
||||
message: response.error,
|
||||
data: null,
|
||||
error_code: String(response.status),
|
||||
};
|
||||
}
|
||||
|
||||
return response.data as QueryCompanyResponse;
|
||||
}
|
||||
Reference in New Issue
Block a user