diff --git a/.env b/.env index 4ad3f36..f7f373b 100644 --- a/.env +++ b/.env @@ -1,3 +1,8 @@ # JWT Secret - 用于签名和验证前端JWT token # 生产环境请务必修改为强随机字符串(至少32个字符) -JWT_SECRET=gdyc-super-secrets-jjwtt-key-change-this-in-production-20250721-from-login-callback \ No newline at end of file +JWT_SECRET=gdyc-super-secrets-jjwtt-key-change-this-in-production-20250721-from-login-callback + +# 交叉评查专属模式 +# 设置为 true 时,端口51707只显示交叉评查入口,隐藏其他模块 +# 设置为 false 时,保持正常模式显示所有模块 +CROSS_CHECKING_ONLY_MODE=false \ No newline at end of file diff --git a/app/api/auth/check-route-permission.server.ts b/app/api/auth/check-route-permission.server.ts new file mode 100644 index 0000000..a615498 --- /dev/null +++ b/app/api/auth/check-route-permission.server.ts @@ -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 { + 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 { + const result = await checkRoutePermission(targetPath, userRole, jwt); + + if (!result.allowed) { + throw new Response(result.error || '无权访问', { status: 403 }); + } +} diff --git a/app/api/corporate-information/qichacha.ts b/app/api/corporate-information/qichacha.ts new file mode 100644 index 0000000..473f02a --- /dev/null +++ b/app/api/corporate-information/qichacha.ts @@ -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 { + // 使用 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('/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 { + 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('/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 { + 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('/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; +} diff --git a/app/components/corporate-information/BusinessInfo.tsx b/app/components/corporate-information/BusinessInfo.tsx new file mode 100644 index 0000000..b21f0b3 --- /dev/null +++ b/app/components/corporate-information/BusinessInfo.tsx @@ -0,0 +1,287 @@ +/** + * 企业工商信息展示组件 + */ + +import type { BusinessInfoProps } from './types'; +import { EntTypeLabel } from './types'; + +/** 格式化日期(移除时间部分) */ +function formatDate(dateStr: string | null | undefined): string { + if (!dateStr) return '-'; + // 移除时间部分,只保留日期 + return dateStr.split(' ')[0] || dateStr; +} + +/** 格式化金额 */ +function formatAmount(amount: string | null | undefined, unit?: string, ccy?: string): string { + if (!amount) return '-'; + const currencyMap: Record = { + 'CNY': '人民币', + 'USD': '美元', + 'HKD': '港币', + 'EUR': '欧元', + }; + const currencyLabel = ccy ? currencyMap[ccy] || ccy : ''; + return `${amount}${unit || ''}${currencyLabel ? ` (${currencyLabel})` : ''}`; +} + +export function BusinessInfo({ data, loading, error }: BusinessInfoProps) { + // 加载中状态 + if (loading) { + return ( +
+
+ + 加载企业信息中... +
+
+ ); + } + + // 错误状态 + if (error) { + return ( +
+
+ + {error} +
+
+ ); + } + + // 无数据状态 + if (!data) { + return ( +
+
+ + 暂无企业工商信息 +
+
+ ); + } + + // 判断是否有注销/吊销信息 + const hasRevokeInfo = data.RevokeInfo && (data.RevokeInfo.CancelDate || data.RevokeInfo.RevokeDate); + + // 获取状态标签颜色 + const getStatusColor = (status: string) => { + if (status === '存续' || status === '在业' || status === '开业') { + return 'bg-green-100 text-green-700 border-green-200'; + } + if (status === '注销' || status === '吊销') { + return 'bg-red-100 text-red-700 border-red-200'; + } + return 'bg-gray-100 text-gray-700 border-gray-200'; + }; + + return ( +
+ {/* 标题栏 */} +
+
+
+ +

企业工商信息

+
+ {data.Status && ( + + {data.Status} + + )} +
+
+ + {/* 企业头部信息 */} +
+
+ {data.ImageUrl && ( + 企业Logo { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + )} +
+

{data.Name}

+
+ {data.CreditCode && ( + + 统一社会信用代码:{data.CreditCode} + + )} + {data.EntType && ( + + {EntTypeLabel[data.EntType] || '其他'} + + )} + {data.IsOnStock === '1' && ( + + {data.StockType}:{data.StockNumber} + + )} +
+ {/* 曾用名 */} + {data.OriginalName && data.OriginalName.length > 0 && ( +
+ 曾用名: + {data.OriginalName.map((item, index) => ( + + {item.Name} + {item.ChangeDate && ({formatDate(item.ChangeDate)})} + + ))} +
+ )} +
+
+
+ + {/* 详细信息 */} +
+ {/* 基本信息 */} +
+

+ + 基本信息 +

+
+ + + + + + + + + {data.Area && ( + + )} +
+
+ + {/* 资本信息 */} +
+

+ + 资本信息 +

+
+ + +
+
+ + {/* 注册地址 */} +
+

+ + 注册地址 +

+

{data.Address || '-'}

+
+ + {/* 经营范围 */} +
+

+ + 经营范围 +

+

+ {data.Scope || '-'} +

+
+ + {/* 委派代表 */} + {data.DesignatedRepresentativeList && data.DesignatedRepresentativeList.length > 0 && ( +
+

+ + 委派代表 +

+
+ + + + + + + + + {data.DesignatedRepresentativeList.map((rep, index) => ( + + + + + ))} + +
合伙人名称委派代表
{rep.PartnerName || '-'}{rep.DelegatedName || '-'}
+
+
+ )} + + {/* 注销/吊销信息 */} + {hasRevokeInfo && ( +
+

+ + 注销/吊销信息 +

+
+
+ {data.RevokeInfo?.CancelDate && ( + <> + + + + )} + {data.RevokeInfo?.RevokeDate && ( + <> + + + + )} +
+
+
+ )} + + {/* 更新时间 */} + {data.UpdatedDate && ( +
+ 数据更新时间:{formatDate(data.UpdatedDate)} +
+ )} +
+
+ ); +} + +/** 信息项组件 */ +interface InfoItemProps { + label: string; + value: string | null | undefined; + labelClassName?: string; +} + +function InfoItem({ label, value, labelClassName = 'text-gray-500' }: InfoItemProps) { + return ( +
+ {label} + {value || '-'} +
+ ); +} diff --git a/app/components/corporate-information/CorporateInfoModal.tsx b/app/components/corporate-information/CorporateInfoModal.tsx new file mode 100644 index 0000000..1048296 --- /dev/null +++ b/app/components/corporate-information/CorporateInfoModal.tsx @@ -0,0 +1,363 @@ +/** + * 企业信息模态框组件 + * 用于展示企业工商信息和失信核查信息 + */ + +import { useState, useEffect, useCallback } from 'react'; +import { CorporateInformation } from './CorporateInformation'; +import { toastService } from '../ui/Toast'; +import { messageService } from '../ui/MessageModal'; +import type { BusinessInfoResult, DishonestyResult, Paging } from './types'; + +export interface CorporateInfoModalProps { + /** 是否显示模态框 */ + visible: boolean; + /** 关闭模态框回调 */ + onClose: () => void; + /** 企业名称(用于标题显示) */ + companyName?: string; + /** 企业工商信息 */ + businessInfo: BusinessInfoResult | null; + /** 失信核查数据 */ + dishonestyInfo: DishonestyResult | null; + /** 失信核查分页信息 */ + dishonestyPaging?: Paging | null; + /** 企业工商信息加载中 */ + businessLoading?: boolean; + /** 失信核查加载中 */ + dishonestyLoading?: boolean; + /** 企业工商信息错误 */ + businessError?: string | null; + /** 失信核查错误 */ + dishonestyError?: string | null; + /** 数据更新时间(ISO格式) */ + updatedAt?: string | null; + /** 强制刷新回调(对接企查查重新查询) */ + onForceRefresh?: () => Promise; +} + +/** + * 格式化更新时间(UTC转北京时间) + * @param isoString ISO格式的时间字符串 + * @returns 格式化后的北京时间字符串 + */ +function formatUpdatedTime(isoString: string | null | undefined): string { + if (!isoString) return '-'; + + try { + const date = new Date(isoString); + + // 检查日期是否有效 + if (isNaN(date.getTime())) { + return '-'; + } + + // 使用 Intl.DateTimeFormat 格式化为北京时间 + const formatter = new Intl.DateTimeFormat('zh-CN', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); + + return formatter.format(date); + } catch { + return '-'; + } +} + +export function CorporateInfoModal({ + visible, + onClose, + companyName, + businessInfo, + dishonestyInfo, + dishonestyPaging, + businessLoading = false, + dishonestyLoading = false, + businessError, + dishonestyError, + updatedAt, + onForceRefresh, +}: CorporateInfoModalProps) { + // 强制刷新按钮加载状态 + const [refreshing, setRefreshing] = useState(false); + + // ESC 键关闭模态框 + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape' && visible) { + onClose(); + } + }, + [visible, onClose] + ); + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [handleKeyDown]); + + // 阻止背景滚动 + useEffect(() => { + if (visible) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + return () => { + document.body.style.overflow = ''; + }; + }, [visible]); + + // 处理强制刷新按钮点击 + const handleForceRefreshClick = () => { + messageService.show({ + title: '确认重新查询', + message: '该操作将对接企查查API进行实时查询,会产生费用(约0.50元/次)。确定要继续吗?', + type: 'warning', + confirmText: '确认查询', + cancelText: '取消', + onConfirm: async () => { + if (onForceRefresh) { + setRefreshing(true); + try { + await onForceRefresh(); + toastService.success('已提交查询,请稍后再查看企业信息'); + onClose(); + } catch (error) { + console.error('强制刷新失败:', error); + toastService.error('查询失败,请稍后重试'); + } finally { + setRefreshing(false); + } + } + }, + }); + }; + + if (!visible) { + return null; + } + + return ( +
{ + // 点击背景关闭 + if (e.target === e.currentTarget) { + onClose(); + } + }} + > +
e.stopPropagation()} + > + {/* 模态框头部 */} +
+
+ +

+ 企业信息查询 +

+ {companyName && ( + + {companyName} + + )} +
+ +
+ + {/* 模态框内容 */} +
+ +
+ + {/* 模态框底部 */} +
+ {/* 左侧:最近查询时间 */} +
+ + 最近查询时间:{formatUpdatedTime(updatedAt)} +
+ + {/* 右侧:按钮组 */} +
+ {/* 对接企查查重新查询按钮 */} + + + {/* 关闭按钮 */} + +
+
+
+
+ ); +} diff --git a/app/components/corporate-information/CorporateInformation.tsx b/app/components/corporate-information/CorporateInformation.tsx new file mode 100644 index 0000000..aaaaf75 --- /dev/null +++ b/app/components/corporate-information/CorporateInformation.tsx @@ -0,0 +1,37 @@ +/** + * 企业综合信息展示组件 + * 整合企业工商信息和失信核查信息 + */ + +import { BusinessInfo } from './BusinessInfo'; +import { DishonestyInfo } from './DishonestyInfo'; +import type { CorporateInformationProps } from './types'; + +export function CorporateInformation({ + businessInfo, + dishonestyInfo, + dishonestyPaging, + businessLoading, + dishonestyLoading, + businessError, + dishonestyError, +}: CorporateInformationProps) { + return ( +
+ {/* 企业工商信息 */} + + + {/* 失信核查信息 */} + +
+ ); +} diff --git a/app/components/corporate-information/DishonestyInfo.tsx b/app/components/corporate-information/DishonestyInfo.tsx new file mode 100644 index 0000000..7e5f2a0 --- /dev/null +++ b/app/components/corporate-information/DishonestyInfo.tsx @@ -0,0 +1,219 @@ +/** + * 失信核查信息展示组件 + */ + +import type { DishonestyInfoProps, DishonestyRecord } from './types'; + +/** 格式化日期 */ +function formatDate(dateStr: string | null | undefined): string { + if (!dateStr) return '-'; + return dateStr; +} + +/** 格式化金额(元转万元) */ +function formatAmount(amountStr: string | null | undefined): string { + if (!amountStr) return '-'; + const amount = parseFloat(amountStr); + if (isNaN(amount)) return amountStr; + if (amount >= 10000) { + return `${(amount / 10000).toFixed(2)}万元`; + } + return `${amount.toFixed(2)}元`; +} + +/** 获取履行情况标签颜色 */ +function getExecuteStatusColor(status: string): string { + if (status === '全部履行') { + return 'bg-green-100 text-green-700 border-green-200'; + } + if (status === '部分履行') { + return 'bg-yellow-100 text-yellow-700 border-yellow-200'; + } + if (status === '全部未履行') { + return 'bg-red-100 text-red-700 border-red-200'; + } + return 'bg-gray-100 text-gray-700 border-gray-200'; +} + +export function DishonestyInfo({ data, paging, loading, error }: DishonestyInfoProps) { + // 加载中状态 + if (loading) { + return ( +
+
+ + 加载失信信息中... +
+
+ ); + } + + // 错误状态 + if (error) { + return ( +
+
+ + {error} +
+
+ ); + } + + // 无失信记录状态 + if (!data || data.VerifyResult === 0 || !data.Data || data.Data.length === 0) { + return ( +
+
+
+ +

失信核查

+
+
+
+
+
+ +
+

暂无失信记录

+

该企业/个人未被列入失信被执行人名单

+
+
+
+ ); + } + + return ( +
+ {/* 标题栏 */} +
+
+
+ +

失信核查

+
+
+ + 存在失信记录 + + {paging && paging.TotalRecords > 0 && ( + + 共 {paging.TotalRecords} 条记录 + + )} +
+
+
+ + {/* 失信记录列表 */} +
+
+ {data.Data.map((record, index) => ( + + ))} +
+ + {/* 分页提示 */} + {paging && paging.TotalRecords > paging.PageSize && ( +
+ 当前显示第 {paging.PageIndex} 页,共 {Math.ceil(paging.TotalRecords / paging.PageSize)} 页 +
+ )} +
+
+ ); +} + +/** 失信记录卡片组件 */ +interface DishonestyRecordCardProps { + record: DishonestyRecord; + index: number; +} + +function DishonestyRecordCard({ record, index }: DishonestyRecordCardProps) { + return ( +
+ {/* 卡片头部 */} +
+
+ + #{index} + + {record.Anno || '案号未知'} +
+ + {record.Executestatus || '状态未知'} + +
+ + {/* 卡片内容 */} +
+
+ + + + + +
+ + {/* 失信行为 */} + {record.ActionRemark && ( +
+
+ +
+ 失信行为 +

+ {record.ActionRemark} +

+
+
+
+ )} +
+
+ ); +} + +/** 记录项组件 */ +interface RecordItemProps { + icon: string; + label: string; + value: string | null | undefined; + className?: string; + valueClassName?: string; +} + +function RecordItem({ icon, label, value, className = '', valueClassName = 'text-gray-800' }: RecordItemProps) { + return ( +
+ +
+ {label} + {value || '-'} +
+
+ ); +} diff --git a/app/components/corporate-information/index.ts b/app/components/corporate-information/index.ts new file mode 100644 index 0000000..e0bae00 --- /dev/null +++ b/app/components/corporate-information/index.ts @@ -0,0 +1,33 @@ +/** + * 企业信息组件导出文件 + */ + +// 主组件 +export { CorporateInformation } from './CorporateInformation'; +export { BusinessInfo } from './BusinessInfo'; +export { DishonestyInfo } from './DishonestyInfo'; +export { CorporateInfoModal } from './CorporateInfoModal'; +export type { CorporateInfoModalProps } from './CorporateInfoModal'; + +// 类型导出 +export type { + // 企业工商信息类型 + DesignatedRepresentative, + OriginalName, + RevokeInfo, + Area, + BusinessInfoResult, + BusinessInfoResponse, + BusinessInfoProps, + // 失信核查类型 + DishonestyRecord, + Paging, + DishonestyResult, + DishonestyResponse, + DishonestyInfoProps, + // 综合组件类型 + CorporateInformationProps, +} from './types'; + +// 枚举和常量导出 +export { EntTypeEnum, EntTypeLabel, ExecuteStatusEnum } from './types'; diff --git a/app/components/corporate-information/types.ts b/app/components/corporate-information/types.ts new file mode 100644 index 0000000..3356621 --- /dev/null +++ b/app/components/corporate-information/types.ts @@ -0,0 +1,273 @@ +/** + * 企业工商信息和失信核查相关类型定义 + */ + +// ==================== 企业工商信息类型 ==================== + +/** 委派代表 */ +export interface DesignatedRepresentative { + /** 合伙人名称 */ + PartnerName: string; + /** 委派代表名称 */ + DelegatedName: string; +} + +/** 曾用名 */ +export interface OriginalName { + /** 曾用名 */ + Name: string; + /** 变更日期 */ + ChangeDate: string; +} + +/** 注销吊销信息 */ +export interface RevokeInfo { + /** 注销日期 */ + CancelDate: string; + /** 注销原因 */ + CancelReason: string; + /** 吊销日期 */ + RevokeDate: string; + /** 吊销原因 */ + RevokeReason: string; +} + +/** 行政区域 */ +export interface Area { + /** 省份 */ + Province: string; + /** 城市 */ + City: string; + /** 区域 */ + County: string; +} + +/** 企业性质枚举 */ +export enum EntTypeEnum { + /** 大陆企业 */ + MainlandEnterprise = '0', + /** 社会组织 */ + SocialOrganization = '1', + /** 事业单位 */ + PublicInstitution = '4', + /** 医院 */ + Hospital = '7', + /** 律师事务所 */ + LawFirm = '9', + /** 学校 */ + School = '10', + /** 机关单位 */ + GovernmentOrgan = '11', + /** 其他 */ + Other = '-1', +} + +/** 企业性质显示映射 */ +export const EntTypeLabel: Record = { + '0': '大陆企业', + '1': '社会组织', + '4': '事业单位', + '7': '医院', + '9': '律师事务所', + '10': '学校', + '11': '机关单位', + '-1': '其他', +}; + +/** 企业工商信息结果 */ +export interface BusinessInfoResult { + /** 主键 */ + KeyNo: string; + /** 企业名称 */ + Name: string; + /** 工商注册号/企业编号 */ + No: string; + /** 登记机关 */ + BelongOrg: string; + /** 法定代表人ID */ + OperId: string; + /** 法定代表人名称 */ + OperName: string; + /** 委派代表列表 */ + DesignatedRepresentativeList: DesignatedRepresentative[]; + /** 成立日期 */ + StartDate: string; + /** 吊销日期(保留字段) */ + EndDate: string; + /** 登记状态 */ + Status: string; + /** 省份(缩写) */ + Province: string; + /** 更新日期 */ + UpdatedDate: string; + /** 统一社会信用代码/商业登记号码 */ + CreditCode: string; + /** 注册资本(含单位) */ + RegistCapi: string; + /** 注册资本数额 */ + RegisteredCapital: string; + /** 注册资本单位 */ + RegisteredCapitalUnit: string; + /** 注册资本币种 */ + RegisteredCapitalCCY: string; + /** 实缴资本(含单位) */ + RecCap: string; + /** 实缴出资额数额 */ + PaidUpCapital: string; + /** 实缴出资额单位 */ + PaidUpCapitalUnit: string; + /** 实缴出资额币种 */ + PaidUpCapitalCCY: string; + /** 企业类型 */ + EconKind: string; + /** 注册地址 */ + Address: string; + /** 经营范围 */ + Scope: string; + /** 营业期限始 */ + TermStart: string; + /** 营业期限至 */ + TermEnd: string; + /** 核准日期 */ + CheckDate: string; + /** 组织机构代码 */ + OrgNo: string; + /** 是否上市(0-未上市,1-上市) */ + IsOnStock: string; + /** 股票代码 */ + StockNumber: string | null; + /** 上市类型 */ + StockType: string | null; + /** 曾用名列表 */ + OriginalName: OriginalName[]; + /** 企业Logo地址 */ + ImageUrl: string; + /** 企业性质 */ + EntType: string; + /** 注销吊销信息 */ + RevokeInfo: RevokeInfo | null; + /** 行政区域 */ + Area: Area | null; + /** 行政区划代码 */ + AreaCode: string; +} + +/** 企业工商信息响应 */ +export interface BusinessInfoResponse { + /** 查询结果数据 */ + Result: BusinessInfoResult | null; + /** 状态码(200 表示成功) */ + Status: string; + /** 响应消息 */ + Message: string; + /** 订单编号 */ + OrderNumber: string; +} + +// ==================== 失信核查类型 ==================== + +/** 失信记录数据 */ +export interface DishonestyRecord { + /** 主键 */ + Id: string; + /** 立案日期 */ + Liandate: string; + /** 案号 */ + Anno: string; + /** 执行法院 */ + Executegov: string; + /** 被执行人的履行情况 */ + Executestatus: string; + /** 发布日期 */ + Publicdate: string; + /** 执行依据文号 */ + Executeno: string; + /** 失信行为 */ + ActionRemark: string; + /** 涉案金额(元) */ + Amount: string; +} + +/** 分页信息 */ +export interface Paging { + /** 每页记录数 */ + PageSize: number; + /** 当前页码 */ + PageIndex: number; + /** 总记录数 */ + TotalRecords: number; +} + +/** 失信核查结果 */ +export interface DishonestyResult { + /** 数据是否存在(1-存在,0-不存在) */ + VerifyResult: number; + /** 数据信息列表 */ + Data: DishonestyRecord[]; +} + +/** 失信核查响应 */ +export interface DishonestyResponse { + /** 分页信息 */ + Paging: Paging; + /** 查询结果数据 */ + Result: DishonestyResult | null; + /** 状态码(200 表示成功) */ + Status: string; + /** 响应消息 */ + Message: string; + /** 订单编号 */ + OrderNumber: string; +} + +/** 履行情况枚举 */ +export enum ExecuteStatusEnum { + /** 全部未履行 */ + NotExecuted = '全部未履行', + /** 部分履行 */ + PartiallyExecuted = '部分履行', + /** 全部履行 */ + FullyExecuted = '全部履行', +} + +// ==================== 组件Props类型 ==================== + +/** 企业工商信息组件Props */ +export interface BusinessInfoProps { + /** 企业工商信息数据 */ + data: BusinessInfoResult | null; + /** 是否加载中 */ + loading?: boolean; + /** 错误信息 */ + error?: string | null; +} + +/** 失信核查组件Props */ +export interface DishonestyInfoProps { + /** 失信核查数据 */ + data: DishonestyResult | null; + /** 分页信息 */ + paging?: Paging | null; + /** 是否加载中 */ + loading?: boolean; + /** 错误信息 */ + error?: string | null; +} + +/** 企业综合信息组件Props */ +export interface CorporateInformationProps { + /** 企业工商信息 */ + businessInfo: BusinessInfoResult | null; + /** 失信核查数据 */ + dishonestyInfo: DishonestyResult | null; + /** 失信核查分页信息 */ + dishonestyPaging?: Paging | null; + /** 企业工商信息加载中 */ + businessLoading?: boolean; + /** 失信核查加载中 */ + dishonestyLoading?: boolean; + /** 企业工商信息错误 */ + businessError?: string | null; + /** 失信核查错误 */ + dishonestyError?: string | null; +} diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 8691fe5..f62fe32 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -21,6 +21,9 @@ import { useState, useEffect, useRef } from 'react'; import { toastService } from '../ui/Toast'; import { createPortal } from 'react-dom'; // 导入React Portal API,用于将组件渲染到DOM树的不同位置 import { Tooltip } from '../ui/Tooltip'; +import { CorporateInfoModal } from '../corporate-information'; +import type { BusinessInfoResult, DishonestyResult } from '../corporate-information'; +import { queryCompanyInfo } from '~/api/corporate-information/qichacha'; // import '../../styles/components/TooltipStyles.css'; /** @@ -423,6 +426,15 @@ export function ReviewPointsList({ // 存放评查点ID与有效页码的映射 const [effectivePages, setEffectivePages] = useState>({}); + // 企业信息模态框状态 + const [corporateModalVisible, setCorporateModalVisible] = useState(false); + const [corporateCompanyName, setCorporateCompanyName] = useState(''); + const [corporateBusinessInfo, setCorporateBusinessInfo] = useState(null); + const [corporateDishonestyInfo, setCorporateDishonestyInfo] = useState(null); + const [corporateLoading, setCorporateLoading] = useState(false); + const [corporateError, setCorporateError] = useState(null); + const [corporateUpdatedAt, setCorporateUpdatedAt] = useState(null); + // 初始化建议文本 useEffect(() => { // 使用函数式更新,不再需要外部 manualReviewNotes 变量 @@ -483,6 +495,71 @@ export function ReviewPointsList({ setEditingReviewPoint(null); }; + /** + * 处理企业信息按钮点击 + * @param companyName 企业名称(乙方名称) + * @param forceRefresh 是否强制刷新(对接企查查重新查询) + */ + const handleCorporateInfoClick = async (companyName: string, forceRefresh = false) => { + if (!companyName) { + toastService.warning('企业名称为空,无法查询'); + return; + } + + // 打开模态框并设置加载状态 + setCorporateModalVisible(true); + setCorporateCompanyName(companyName); + setCorporateLoading(true); + setCorporateError(null); + setCorporateBusinessInfo(null); + setCorporateDishonestyInfo(null); + setCorporateUpdatedAt(null); + + try { + const response = await queryCompanyInfo({ keyword: companyName, force_refresh: forceRefresh }); + + if (response.success && response.data) { + setCorporateBusinessInfo(response.data.enterprise); + setCorporateUpdatedAt(response.data.updated_at); + // 转换失信数据格式 + if (response.data.dishonesty) { + setCorporateDishonestyInfo({ + VerifyResult: response.data.dishonesty.VerifyResult, + Data: response.data.dishonesty.Data || [], + }); + } + } else { + setCorporateError(response.message || '查询失败'); + } + } catch (error) { + console.error('查询企业信息失败:', error); + setCorporateError(error instanceof Error ? error.message : '查询失败'); + } finally { + setCorporateLoading(false); + } + }; + + /** + * 处理强制刷新(对接企查查重新查询) + */ + const handleCorporateForceRefresh = async () => { + if (corporateCompanyName) { + await handleCorporateInfoClick(corporateCompanyName, true); + } + }; + + /** + * 关闭企业信息模态框 + */ + const handleCloseCorporateModal = () => { + setCorporateModalVisible(false); + setCorporateCompanyName(''); + setCorporateBusinessInfo(null); + setCorporateDishonestyInfo(null); + setCorporateError(null); + setCorporateUpdatedAt(null); + }; + /** * 过滤评查点 * 根据搜索文本和状态过滤条件筛选评查点 @@ -2406,7 +2483,7 @@ export function ReviewPointsList({ tabIndex={0} style={{ userSelect: 'text' }} onClick={() => { - // console.log('reviewPoint', reviewPoint); + console.log('reviewPoint', reviewPoint); handleReviewPointClick(reviewPoint.id); }} onKeyDown={(e) => { @@ -2419,7 +2496,51 @@ export function ReviewPointsList({ {/* 评查点名称 pointName*/}
{/*
*/} -
{reviewPoint.pointName}
+
+
{reviewPoint.pointName}
+ { reviewPoint.pointName === '签署乙方详细信息校验' && ( + + )} + +
{/*
{reviewPoint.title}
//评查点分组显示 @@ -2450,6 +2571,21 @@ export function ReviewPointsList({ renderEmptyState() )}
+ + {/* 企业信息模态框 */} +
); } \ No newline at end of file diff --git a/app/config/api-config.ts b/app/config/api-config.ts index 10bb87a..1a685ea 100644 --- a/app/config/api-config.ts +++ b/app/config/api-config.ts @@ -450,4 +450,20 @@ if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'testing' } }, }); -} \ No newline at end of file +} + +/** + * 交叉评查专属模式配置 + * 当为 true 且端口为 51707 时,首页只显示交叉评查入口 + */ +export const CROSS_CHECKING_ONLY_MODE = process.env.CROSS_CHECKING_ONLY_MODE === 'true'; + +/** + * 交叉评查专属模式的目标端口 + */ +export const CROSS_CHECKING_ONLY_PORT = '51707'; + +/** + * 获取当前端口(服务端使用) + */ +export { getCurrentPort }; \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index d8f96f0..a686caf 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -443,7 +443,11 @@ export function ErrorBoundary() { statusText={statusText} message={message} /> + {/* 添加 RouteChangeLoader 确保导航状态变化时能正确隐藏加载条 */} + + {/* 添加 LoadingBarContainer 确保加载条能正确显示和隐藏 */} + ); diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index f11e527..ebaf81e 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -5,7 +5,7 @@ import styles from "~/styles/pages/home.css?url"; import dayjs from 'dayjs'; import { getUserSession, logout } from "~/api/login/auth.server"; import { toastService } from '~/components/ui'; -import { DOCUMENT_URL } from '~/config/api-config'; +import { DOCUMENT_URL, CROSS_CHECKING_ONLY_MODE, CROSS_CHECKING_ONLY_PORT, getCurrentPort } from '~/config/api-config'; export const links = () => [ { rel: "stylesheet", href: styles } @@ -87,8 +87,26 @@ export async function loader({ request }: LoaderFunctionArgs) { } } + // 🔑 判断是否启用交叉评查专属模式 + // 条件:CROSS_CHECKING_ONLY_MODE=true 且 当前端口为 51707 + const currentPort = getCurrentPort(); + const isCrossCheckingOnlyMode = CROSS_CHECKING_ONLY_MODE && currentPort === CROSS_CHECKING_ONLY_PORT; + + if (isCrossCheckingOnlyMode) { + console.log(`🔒 [Index Loader] 交叉评查专属模式已启用 (端口: ${currentPort})`); + } + // 返回用户信息、入口模块和权限给客户端 - return Response.json({ userRole, userInfo, entryModules, hasSettingsAccess, hasCrossCheckingAccess, hasChatLLMAccess, settingsChildren }); + return Response.json({ + userRole, + userInfo, + entryModules, + hasSettingsAccess, + hasCrossCheckingAccess, + hasChatLLMAccess, + settingsChildren, + isCrossCheckingOnlyMode // 新增:交叉评查专属模式标志 + }); } export default function Index() { @@ -326,8 +344,8 @@ export default function Index() {
- {/* 系统设置按钮 - 只在有权限时显示 */} - {loaderData.hasSettingsAccess && ( + {/* 系统设置按钮 - 只在有权限且非交叉评查专属模式时显示 */} + {loaderData.hasSettingsAccess && !loaderData.isCrossCheckingOnlyMode && (
diff --git a/app/routes/contract-template.detail.$id.tsx b/app/routes/contract-template.detail.$id.tsx index aef43e7..b11a728 100644 --- a/app/routes/contract-template.detail.$id.tsx +++ b/app/routes/contract-template.detail.$id.tsx @@ -1,6 +1,6 @@ import type { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'; import { redirect } from '@remix-run/node'; -import { useLoaderData, useNavigate, useSubmit } from '@remix-run/react'; +import { useLoaderData, useNavigate, useSubmit, useActionData } from '@remix-run/react'; import { useState, useEffect } from 'react'; import { getContractTemplate } from '~/api/contract-template/templates'; import type { ContractTemplate } from '~/api/contract-template/templates'; @@ -9,6 +9,7 @@ import filePreviewStyles from '~/styles/components/file-preview-isolation.css?ur import { getUserSession } from '~/api/login/auth.server'; import { createDraftContract } from '~/api/contracts/draft-service.server'; import { apiRequest, downloadFile } from '~/api/axios-client'; +import { checkRoutePermission } from '~/api/auth/check-route-permission.server'; // 导入FilePreview组件 import { FilePreview } from '~/components/reviews'; @@ -76,11 +77,20 @@ export async function action({ request, params }: ActionFunctionArgs) { } // 获取用户信息和JWT - const { userInfo, frontendJWT } = await getUserSession(request); + const { userInfo, frontendJWT, userRole } = await getUserSession(request); if (!userInfo?.sub) { return Response.json({ error: '未登录' }, { status: 401 }); } + // 🔒 在执行任何操作之前,先检查用户是否有权限访问目标路由 + const targetPath = '/contract-draft'; + const permissionCheck = await checkRoutePermission(targetPath, userRole, frontendJWT || undefined); + + if (!permissionCheck.allowed) { + console.warn(`[Action] 用户无权访问 ${targetPath}:`, permissionCheck.error); + return Response.json({ error: permissionCheck.error || '您没有权限使用起草合同功能' }, { status: 403 }); + } + try { // 解析表单数据 const formData = await request.formData(); @@ -148,8 +158,14 @@ export async function action({ request, params }: ActionFunctionArgs) { } } +// Action 返回的数据类型 +interface ActionData { + error?: string; +} + export default function ContractTemplateDetail() { const { template }: { template: ContractTemplate } = useLoaderData(); + const actionData = useActionData(); const navigate = useNavigate(); const submit = useSubmit(); const [isCreatingDraft, setIsCreatingDraft] = useState(false); @@ -162,6 +178,14 @@ export default function ContractTemplateDetail() { window.scrollTo({ top: 0, behavior: 'instant' }); }, []); + // 处理 action 返回的错误 + useEffect(() => { + if (actionData?.error) { + toastService.error(actionData.error); + setIsCreatingDraft(false); + } + }, [actionData]); + const handleBack = () => { navigate('/contract-template/list'); }; diff --git a/app/routes/cross-checking._index.tsx b/app/routes/cross-checking._index.tsx index 3f9a270..da2d9d4 100644 --- a/app/routes/cross-checking._index.tsx +++ b/app/routes/cross-checking._index.tsx @@ -196,7 +196,7 @@ export async function action({ request }: ActionFunctionArgs) { }, { status: 500 }); } - console.log('用户任务详情返回:', response.data); + // console.log('用户任务详情返回:', response.data); return Response.json({ success: true, data: response.data diff --git a/app/routes/document-types._index.tsx b/app/routes/document-types._index.tsx index 895edbf..2676c26 100644 --- a/app/routes/document-types._index.tsx +++ b/app/routes/document-types._index.tsx @@ -163,6 +163,7 @@ export default function DocumentTypesList() { const canCreateType = canCreate('document_type'); const canUpdateType = canUpdate('document_type'); const canDeleteType = canDelete('document_type'); + // console.log('document_type---canDeleteType',canDeleteType) const canViewType = canView('document_type'); // 获取搜索参数 @@ -299,7 +300,7 @@ export default function DocumentTypesList() { { title: "文档类型名称", key: "name", - width: "200px", + width: "220px", render: (_: unknown, record: DocumentTypeUI) => (
@@ -310,10 +311,10 @@ export default function DocumentTypesList() { { title: "描述", key: "description", - width: "250px", + width: "260px", render: (_: unknown, record: DocumentTypeUI) => ( -
- {record.description} +
+ {record.description || '-'}
) }, @@ -326,7 +327,7 @@ export default function DocumentTypesList() { {record.entry_module ? ( {record.entry_module.name} ) : ( - 暂无关联入口 + - )}
) @@ -343,7 +344,7 @@ export default function DocumentTypesList() { )) ) : ( - 暂无关联分组 + - )}
) @@ -351,19 +352,19 @@ export default function DocumentTypesList() { { title: "创建时间", key: "created_at", - width: "150px", - render: (_: unknown, record: DocumentTypeUI) => record.created_at + width: "160px", + render: (_: unknown, record: DocumentTypeUI) => record.created_at || '-' }, { title: "更新时间", key: "updated_at", - width: "150px", - render: (_: unknown, record: DocumentTypeUI) => record.updated_at + width: "160px", + render: (_: unknown, record: DocumentTypeUI) => record.updated_at || '-' }, { title: "操作", key: "operation", - width: "150px", + width: "160px", render: (_: unknown, record: DocumentTypeUI) => (
{canViewType && ( @@ -379,7 +380,7 @@ export default function DocumentTypesList() { )} {canDeleteType && ( - + {/* 保存修改按钮 - 需要 document:document:update 权限 */} + {canUpdate && ( + + )}
diff --git a/app/routes/documents.list.tsx b/app/routes/documents.list.tsx index 3fafa17..10fc265 100644 --- a/app/routes/documents.list.tsx +++ b/app/routes/documents.list.tsx @@ -3,6 +3,7 @@ import { useSearchParams, useLoaderData, useFetcher, useNavigate,Link } from "@r import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; +import { usePermission, PermissionGuard } from "~/hooks/usePermission"; // import { Table } from "~/components/ui/Table"; import { Pagination } from "~/components/ui/Pagination"; import { FileTypeTag } from "~/components/ui/FileTypeTag"; @@ -184,6 +185,11 @@ export default function DocumentsIndex() { const fetcher = useFetcher(); const navigate = useNavigate(); + // 权限控制 + const { hasPermission } = usePermission(); + const canView = hasPermission('document:document:view'); + const canUpdate = hasPermission('document:document:update'); + // 存储从 sessionStorage 获取的 documentTypeIds const [documentTypeIds, setDocumentTypeIds] = useState(null); @@ -1172,29 +1178,39 @@ export default function DocumentsIndex() { {historyDoc.uploadTime}
- - - 查看 - - - - 修改 - - - {parentDoc.type === '1' && historyDoc.fileStatus === 'Processed' && ( + {/* 查看按钮 - 需要 document:document:view 权限 */} + {canView && ( + + + 查看 + + )} + {/* 修改按钮 - 需要 document:document:view 权限 */} + {canView && ( + + + 修改 + + )} + {/* 下载按钮 - 需要 document:document:view 权限 */} + {canView && ( + + )} + {/* 追加附件和上传模板按钮 - 需要 document:document:view 权限 */} + {canView && parentDoc.type === '1' && historyDoc.fileStatus === 'Processed' && ( <> + {/* 删除按钮 - 需要 document:document:view 权限 */} + {canView && ( + + )}
@@ -1393,53 +1412,65 @@ export default function DocumentsIndex() { width: "25%", render: (_: unknown, record: DocumentUI) => (
- {(record.auditStatus === 0 || record.auditStatus == null) ? ( - + ) : record.auditStatus === 3 ? ( + //record.auditStatus === 3 目前这个状态不存在,所以除了待审核(0)-开始审核,其他都是审核中(2)-查看 + + + 查看进度 + + ) : ( + + + 查看 + + )} + + )} + {/* 修改按钮 - 需要 document:document:view 权限 */} + {canView && ( + - - 开始审核 - - ) : record.auditStatus === 3 ? ( - //record.auditStatus === 3 目前这个状态不存在,所以除了待审核(0)-开始审核,其他都是审核中(2)-查看 - - - 查看进度 - - ) : ( - - - 查看 + + 修改 )} - - - 修改 - - - {record.type === '1' && record.fileStatus === 'Processed' && ( + {/* 下载按钮 - 需要 document:document:view 权限 */} + {canView && ( + + )} + {/* 追加附件和上传模板按钮 - 需要 document:document:view 权限 */} + {canView && record.type === '1' && record.fileStatus === 'Processed' && ( <> + {/* 删除按钮 - 需要 document:document:view 权限 */} + {canView && ( + + )}
) } diff --git a/app/routes/home.tsx b/app/routes/home.tsx index e4bfc23..dc8366d 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -448,8 +448,8 @@ export default function Home() { )} - {/* 高风险用户 */} - {topRiskUsers.available && ( + {/* 高风险用户 隐藏*/} + {topRiskUsers.available && false &&( **注意**:仅调用企查查API时产生费用,使用数据库记录不产生费用。 + +### 权限要求 + +|权限键 |说明 |分配角色 | +|---|---|---| +|`qichacha:company:query` |查询企业信息 |市级管理员、省级管理员、普通员工 | +|`qichacha:status:read` |查看记录状态 |市级管理员、省级管理员、普通员工 | + + +--- + +## 业务逻辑 + +### 核心流程 + +企查查模块的核心设计理念是**数据库优先**,通过本地数据库存储已查询过的企业信息,减少对企查查API的调用次数,从而节省费用。 + +#### 查询流程图 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 用户发起查询请求 │ +│ keyword = "腾讯科技" 或 信用代码 │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ + ┌────────────────────────┐ + │ 1. 查询数据库 │ + │ (三字段OR匹配) │ + │ search_key = keyword │ + │ OR credit_code = keyword │ + │ OR company_name = keyword │ + └────────────────────────┘ + │ + ┌────────────┴────────────┐ + │ │ + ▼ ▼ + ┌──────────┐ ┌──────────┐ + │ 无记录 │ │ 有记录 │ + └──────────┘ └──────────┘ + │ │ + │ ▼ + │ ┌─────────────────────────┐ + │ │ 2. 检查force_refresh │ + │ │ 参数是否为true? │ + │ └─────────────────────────┘ + │ │ │ + │ true false + │ │ │ + │ │ ▼ + │ │ ┌──────────────────┐ + │ │ │ 3. 检查数据新鲜度 │ + │ │ │ updated_at距今 │ + │ │ │ 是否超过30天? │ + │ │ └──────────────────┘ + │ │ │ │ + │ │ >=30天 <30天 + │ │ │ │ + ▼ ▼ ▼ ▼ + ┌─────────────────────────────┐ ┌──────────────┐ + │ 4. 调用企查查API │ │ 5. 直接返回 │ + │ ├─ 410接口(工商) 0.20元 │ │ 数据库记录 │ + │ └─ 740接口(失信) 0.30元 │ │ │ + │ │ │ ✓ 不产生费用 │ + │ 5. 写入/更新数据库 │ │ ✓ 响应更快 │ + │ ├─ search_key = keyword │ └──────────────┘ + │ ├─ credit_code (从API获取) │ + │ ├─ company_name (从API获取)│ + │ ├─ enterprise = JSON │ + │ └─ dishonesty = JSON │ + │ │ + │ 6. 返回查询结果 │ + │ ✗ 产生费用: 0.50元/次 │ + └─────────────────────────────┘ +``` + +### 数据刷新策略 + +|场景 |条件 |动作 |是否产生费用 | +|---|---|---|---| +|首次查询 |数据库无此企业记录 |调用API → 写入数据库 |✓ 0.50元 | +|强制刷新 |`force_refresh=true` |调用API → 更新数据库 |✓ 0.50元 | +|数据过期 |记录存在但超过30天 |调用API → 更新数据库 |✓ 0.50元 | +|数据有效 |记录存在且未超30天 |直接返回数据库记录 |✗ 免费 | + +### 三字段匹配机制 + +数据库查询使用**OR逻辑**匹配以下三个字段: + +|字段 |说明 |示例 | +|---|---|---| +|`search_key` |原始查询关键词 |"腾讯科技" | +|`credit_code` |统一社会信用代码 |"91440300708461136T" | +|`company_name` |企业全称(从API获取) |"腾讯科技(深圳)有限公司" | + +**匹配示例:** + + +1. 用户首次用 `"腾讯科技"` 查询 + - 数据库无记录 → 调用API + - API返回企业全称和信用代码 + - 写入数据库:`search_key="腾讯科技"`, `credit_code="91440300708461136T"`, `company_name="腾讯科技(深圳)有限公司"` +2. 用户再用 `"91440300708461136T"` 查询 + - 数据库查询:`WHERE search_key='91440300708461136T' OR credit_code='91440300708461136T' OR company_name='91440300708461136T'` + - 命中 `credit_code` 字段 → 返回已有记录(不调用API) +3. 用户再用 `"腾讯科技(深圳)有限公司"` 查询 + - 命中 `company_name` 字段 → 返回已有记录(不调用API) + +### 数据存储结构 + +数据库表:`qcc_company_info` + +|字段 |类型 |说明 | +|---|---|---| +|id |SERIAL |主键 | +|search_key |VARCHAR(500) |查询关键词(唯一索引) | +|credit_code |VARCHAR(50) |统一社会信用代码 | +|company_name |VARCHAR(500) |企业名称 | +|enterprise |JSONB |410接口原始返回(工商信息) | +|dishonesty |JSONB |740接口原始返回(失信信息) | +|created_at |TIMESTAMPTZ |创建时间 | +|updated_at |TIMESTAMPTZ |更新时间(用于判断数据新鲜度) | + +### API调用说明 + +#### 企查查接口 + +|接口代码 |接口名称 |请求路径 |单价 | +|---|---|---|---| +|410 |企业工商信息 |`/ECIV4/GetBasicDetailsByName` |0.20元/次 | +|740 |失信核查 |`/ShixinCheck/GetList` |0.30元/次 | + +#### Token生成规则 + +``` +Token = MD5(AppKey + Timespan + SecretKey).toUpperCase() +``` + +- `AppKey`: 企查查分配的应用Key +- `Timespan`: Unix时间戳(秒) +- `SecretKey`: 企查查分配的密钥 + +#### 请求头 + +|Header |值 | +|---|---| +|Token |MD5签名(大写) | +|Timespan |Unix时间戳 | +|Content-Type |application/json | + +### 并发处理 + +查询完整企业信息时,系统会**并发调用**410和740两个接口: + +```python +# 并发请求示例 +enterprise_task = client.get_enterprise_info(keyword) # 410接口 +dishonesty_task = client.get_dishonesty_info(keyword) # 740接口 + +enterprise_resp, dishonesty_resp = await asyncio.gather( + enterprise_task, + dishonesty_task, + return_exceptions=True +) +``` + +优点: + +- 减少总响应时间(两个请求并行执行) +- 单个接口失败不影响另一个接口的结果 + + +--- + +## API 接口 + +### 1. 查询企业信息 + +查询企业完整信息(工商信息 + 失信核查) + +**请求** + +``` +POST /api/v2/qichacha/company +Content-Type: application/json +Authorization: Bearer +``` + +**请求参数** + +|字段 |类型 |必填 |说明 | +|---|---|---|---| +|keyword |string |是 |查询关键词(企业名称或统一社会信用代码),2-200字符 | +|force_refresh |boolean |否 |是否强制刷新,默认false | + +**请求示例** + +```json +{ + "keyword": "腾讯科技(深圳)有限公司", + "force_refresh": false +} +``` + +**响应参数** + +|字段 |类型 |说明 | +|---|---|---| +|success |boolean |查询是否成功 | +|message |string |响应消息 | +|data |object |企业信息(见下表) | +|error_code |string |错误码(失败时返回) | +|error_details |object |错误详情(失败时返回) | + +**data 字段说明** + +|字段 |类型 |说明 | +|---|---|---| +|search_key |string |查询关键词 | +|credit_code |string |统一社会信用代码 | +|company_name |string |企业名称 | +|enterprise |object |企业工商信息(410接口原始返回) | +|dishonesty |object |失信核查信息(740接口原始返回) | +|has_dishonesty |boolean |是否有失信记录 | +|dishonesty_count |integer |失信记录数量 | +|updated_at |datetime |数据更新时间 | + +**enterprise 字段说明(410接口返回)** + +|字段 |类型 |说明 | +|---|---|---| +|KeyNo |string |企查查内部KeyNo | +|Name |string |企业名称 | +|No |string |工商注册号 | +|BelongOrg |string |登记机关 | +|OperId |string |法定代表人ID | +|OperName |string |法定代表人姓名 | +|DesignatedRepresentativeList |array |指定代表人列表 | +|StartDate |string |成立日期(格式:YYYY-MM-DD HH:mm:ss) | +|EndDate |string |注销日期(空字符串表示未注销) | +|Status |string |企业状态(存续、注销、吊销等) | +|Province |string |省份代码(如JS=江苏) | +|UpdatedDate |string |数据更新日期 | +|CreditCode |string |统一社会信用代码(部分老企业可能为空) | +|RegistCapi |string |注册资本(含单位,如"36225万元") | +|RegisteredCapital |string |注册资本数值 | +|RegisteredCapitalUnit |string |注册资本单位(如"万") | +|RegisteredCapitalCCY |string |注册资本币种(如"CNY") | +|EconKind |string |企业类型(如"有限责任公司"、"全民所有制"等) | +|Address |string |注册地址 | +|Scope |string |经营范围 | +|TermStart |string |营业期限起始日期 | +|TermEnd |string |营业期限终止日期 | +|CheckDate |string |核准日期 | +|OrgNo |string |组织机构代码 | +|IsOnStock |string |是否上市("0"=否,"1"=是) | +|StockNumber |string |股票代码(非上市为null) | +|StockType |string |股票类型(非上市为null) | +|OriginalName |array |曾用名列表 | +|ImageUrl |string |企业Logo图片URL | +|EntType |string |企业类型代码 | +|RecCap |string |实缴资本(含单位) | +|PaidUpCapital |string |实缴资本数值 | +|PaidUpCapitalUnit |string |实缴资本单位 | +|PaidUpCapitalCCY |string |实缴资本币种 | +|RevokeInfo |object |注销/吊销信息 | +|Area |object |地区信息 | +|AreaCode |string |地区代码 | + +**enterprise.DesignatedRepresentativeList[] 指定代表人** + +|字段 |类型 |说明 | +|---|---|---| +|PartnerName |string |股东名称 | +|DelegatedName |string |委派代表姓名 | + +**enterprise.OriginalName[] 曾用名** + +|字段 |类型 |说明 | +|---|---|---| +|Name |string |曾用名称 | +|ChangeDate |string |变更日期 | + +**enterprise.RevokeInfo 注销/吊销信息** + +|字段 |类型 |说明 | +|---|---|---| +|CancelDate |string |注销日期 | +|CancelReason |string |注销原因 | +|RevokeDate |string |吊销日期 | +|RevokeReason |string |吊销原因 | + +**enterprise.Area 地区信息** + +|字段 |类型 |说明 | +|---|---|---| +|Province |string |省份 | +|City |string |城市 | +|County |string |区县 | + +**dishonesty 字段说明(740接口返回)** + +|字段 |类型 |说明 | +|---|---|---| +|VerifyResult |integer |核查结果:0-无失信记录,1-有失信记录 | +|Data |array |失信记录列表(VerifyResult=0时为null或空数组) | + +**dishonesty.Data[] 失信记录字段说明** + +|字段 |类型 |说明 | +|---|---|---| +|Id |string |记录唯一标识 | +|Liandate |string |立案日期(格式:YYYY-MM-DD) | +|Anno |string |案号 | +|Executegov |string |执行法院 | +|Executestatus |string |执行情况(全部未履行/部分履行/全部履行) | +|Publicdate |string |发布日期(格式:YYYY-MM-DD) | +|Executeno |string |执行依据文号 | +|ActionRemark |string |失信被执行人行为具体情形 | +|Amount |string |执行标的金额(元) | + +**响应示例(成功)** + +```json +{ + "success": true, + "message": "查询成功", + "data": { + "search_key": "腾讯科技(深圳)有限公司", + "credit_code": "91440300708461136T", + "company_name": "腾讯科技(深圳)有限公司", + "enterprise": { + "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" + }, + "dishonesty": { + "VerifyResult": 0, + "Data": null + }, + "has_dishonesty": false, + "dishonesty_count": 0, + "updated_at": "2024-12-12T10:30:00+08:00" + } +} +``` + +**响应示例(有失信记录)** + +```json +{ + "success": true, + "message": "查询成功", + "data": { + "search_key": "某某建设工程有限公司", + "credit_code": "91440106MA5CXXXX12", + "company_name": "某某建设工程有限公司", + "enterprise": { + "KeyNo": "abc123def456ghi789jkl", + "Name": "某某建设工程有限公司", + "No": "440106000123456", + "BelongOrg": "广州市天河区市场监督管理局", + "OperId": "abc123def456ghi789jklop", + "OperName": "张三", + "DesignatedRepresentativeList": [], + "StartDate": "2018-05-10 00:00:00", + "EndDate": "", + "Status": "存续", + "Province": "GD", + "UpdatedDate": "2024-12-01 09:00:00", + "CreditCode": "91440106MA5CXXXX12", + "RegistCapi": "5000万人民币", + "RegisteredCapital": "5000", + "RegisteredCapitalUnit": "万", + "RegisteredCapitalCCY": "CNY", + "EconKind": "有限责任公司(自然人投资或控股)", + "Address": "广州市天河区某某路123号", + "Scope": "建筑工程施工;市政公用工程施工;房屋建筑工程施工;建筑装饰、装修工程施工;园林绿化工程施工", + "TermStart": "2018-05-10 00:00:00", + "TermEnd": "2048-05-09 00:00:00", + "CheckDate": "2023-06-20 00:00:00", + "OrgNo": "MA5CXXXX1", + "IsOnStock": "0", + "StockNumber": null, + "StockType": null, + "OriginalName": [], + "ImageUrl": "", + "EntType": "1", + "RecCap": "1000万人民币", + "PaidUpCapital": "1000", + "PaidUpCapitalUnit": "万", + "PaidUpCapitalCCY": "CNY", + "RevokeInfo": { + "CancelDate": "", + "CancelReason": "", + "RevokeDate": "", + "RevokeReason": "" + }, + "Area": { + "Province": "广东省", + "City": "广州市", + "County": "天河区" + }, + "AreaCode": "440106" + }, + "dishonesty": { + "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" + } + ] + }, + "has_dishonesty": true, + "dishonesty_count": 1, + "updated_at": "2024-12-12T10:30:00+08:00" + } +} +``` + +**响应示例(失败)** + +```json +{ + "success": false, + "message": "当前KEY已欠费", + "data": null, + "error_code": "102", + "error_details": { + "order_number": "20241212103000123", + "is_charged": false, + "is_valid_request": false + } +} +``` + + +--- + +### 2. 仅查询工商信息 + +仅查询企业工商信息(410接口),费用 0.20元/次 + +**请求** + +``` +POST /api/v2/qichacha/enterprise +Content-Type: application/json +Authorization: Bearer +``` + +**请求参数** + +同 [查询企业信息](#1-查询企业信息) + +**响应参数** + +同 [查询企业信息](#1-查询企业信息),但 `dishonesty` 字段为 null + + +--- + +### 3. 仅查询失信信息 + +仅查询企业失信信息(740接口),费用 0.30元/次 + +**请求** + +``` +POST /api/v2/qichacha/dishonesty +Content-Type: application/json +Authorization: Bearer +``` + +**请求参数** + +同 [查询企业信息](#1-查询企业信息) + +**响应参数** + +同 [查询企业信息](#1-查询企业信息),但 `enterprise` 字段为 null + + +--- + +### 4. 批量查询企业信息 + +批量查询多个企业信息,单次最多10个 + +**请求** + +``` +POST /api/v2/qichacha/batch +Content-Type: application/json +Authorization: Bearer +``` + +**请求参数** + +|字段 |类型 |必填 |说明 | +|---|---|---|---| +|keywords |array[string] |是 |查询关键词列表,1-10个 | +|force_refresh |boolean |否 |是否强制刷新,默认false | + +**请求示例** + +```json +{ + "keywords": ["腾讯科技", "阿里巴巴", "华为技术"], + "force_refresh": false +} +``` + +**响应参数** + +|字段 |类型 |说明 | +|---|---|---| +|success |boolean |是否全部成功 | +|total |integer |总查询数 | +|success_count |integer |成功数 | +|failed_count |integer |失败数 | +|results |array |查询结果列表(每个元素同单个查询响应) | + +**响应示例** + +> 注:`results` 数组中每个元素的 `data` 结构与单个查询响应的 `data` 字段完全一致,包含完整的 `enterprise` 和 `dishonesty` 对象。 + +```json +{ + "success": true, + "total": 3, + "success_count": 3, + "failed_count": 0, + "results": [ + { + "success": true, + "message": "查询成功", + "data": { + "search_key": "腾讯科技", + "credit_code": "91440300708461136T", + "company_name": "腾讯科技(深圳)有限公司", + "enterprise": { + "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": [], + "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" + }, + "dishonesty": { + "VerifyResult": 0, + "Data": null + }, + "has_dishonesty": false, + "dishonesty_count": 0, + "updated_at": "2024-12-12T10:30:00+08:00" + } + }, + { + "success": true, + "message": "查询成功", + "data": { + "search_key": "阿里巴巴", + "credit_code": "91330100799655058B", + "company_name": "阿里巴巴(中国)有限公司", + "enterprise": { "/* 完整enterprise对象,结构同上 */" : "..." }, + "dishonesty": { "VerifyResult": 0, "Data": null }, + "has_dishonesty": false, + "dishonesty_count": 0, + "updated_at": "2024-12-12T10:31:00+08:00" + } + }, + { + "success": true, + "message": "查询成功", + "data": { + "search_key": "华为技术", + "credit_code": "914403006194776951", + "company_name": "华为技术有限公司", + "enterprise": { "/* 完整enterprise对象,结构同上 */" : "..." }, + "dishonesty": { "VerifyResult": 0, "Data": null }, + "has_dishonesty": false, + "dishonesty_count": 0, + "updated_at": "2024-12-12T10:32:00+08:00" + } + } + ] +} +``` + + +--- + +### 5. 查询数据库记录状态 + +查询企业在数据库中的记录状态(不调用企查查API,不产生费用) + +**请求** + +``` +GET /api/v2/qichacha/status?keyword=腾讯科技 +Authorization: Bearer +``` + +**请求参数** + +|字段 |类型 |必填 |说明 | +|---|---|---|---| +|keyword |string |是 |查询关键词,最少2字符 | + +**响应参数** + +|字段 |类型 |说明 | +|---|---|---| +|exists |boolean |记录是否存在 | +|keyword |string |查询关键词 | +|search_key |string |数据库中的search_key | +|credit_code |string |统一社会信用代码 | +|company_name |string |企业名称 | +|has_enterprise |boolean |是否有工商信息 | +|has_dishonesty |boolean |是否有失信信息 | +|updated_at |datetime |数据更新时间 | +|age_days |integer |数据已存在天数 | +|refresh_threshold_days |integer |刷新阈值(默认30天) | +|need_refresh |boolean |是否需要刷新 | + +**响应示例(有记录)** + +```json +{ + "exists": true, + "keyword": "腾讯科技", + "search_key": "腾讯科技(深圳)有限公司", + "credit_code": "91440300708461136T", + "company_name": "腾讯科技(深圳)有限公司", + "has_enterprise": true, + "has_dishonesty": true, + "updated_at": "2024-12-01T10:30:00+08:00", + "age_days": 11, + "refresh_threshold_days": 30, + "need_refresh": false +} +``` + +**响应示例(无记录)** + +```json +{ + "exists": false, + "keyword": "某某不存在的公司" +} +``` + + +--- + +## 错误码 + +### 企查查API状态码 + +#### 有效请求(会计费) + +|状态码 |说明 | +|---|---| +|200 |查询成功 | +|201 |查询无结果 | +|202 |查询参数错误 | +|205 |等待处理中 | +|207 |请求数据条目数超过上限 | +|208 |此接口不支持此公司类型查询 | +|209 |企业数量超过上限 | +|213 |参数长度不能小于2 | +|215 |不支持的查询关键字 | + +#### 无效请求(不计费) + +|状态码 |说明 | +|---|---| +|101 |当前的KEY无效或还未生效 | +|102 |当前KEY已欠费 | +|103 |当前KEY被暂停使用 | +|107 |被禁止的IP或签名错误 | +|111 |接口权限未开通 | +|112 |账号剩余使用量不足或已过期 | + +### HTTP状态码 + +|状态码 |说明 | +|---|---| +|200 |成功 | +|401 |未认证(Token无效或过期) | +|403 |无权限(缺少qichacha:company:query权限) | +|500 |服务器内部错误 | + + +--- + +## 配置说明 + +在环境配置文件中添加: + +```ini +# 企查查API配置 +QCC_APP_KEY=你的AppKey +QCC_SECRET_KEY=你的SecretKey + +# 可选配置 +QCC_BASE_URL=https://api.qichacha.com +QCC_TIMEOUT=30 +QCC_CACHE_DAYS=30 +``` + + +--- + +## 使用示例 + +### cURL + +```bash +# 查询企业信息 +curl -X POST "http://localhost:8000/api/v2/qichacha/company" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"keyword": "腾讯科技", "force_refresh": false}' + +# 批量查询 +curl -X POST "http://localhost:8000/api/v2/qichacha/batch" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"keywords": ["腾讯", "阿里", "华为"]}' + +# 查询记录状态 +curl -X GET "http://localhost:8000/api/v2/qichacha/status?keyword=腾讯" \ + -H "Authorization: Bearer " +``` + +### Python + +```python +import httpx + +async def query_company(keyword: str, token: str): + async with httpx.AsyncClient() as client: + response = await client.post( + "http://localhost:8000/api/v2/qichacha/company", + json={"keyword": keyword, "force_refresh": False}, + headers={"Authorization": f"Bearer {token}"} + ) + return response.json() +``` + +### JavaScript + +```javascript +async function queryCompany(keyword, token) { + const response = await fetch('/api/v2/qichacha/company', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + keyword: keyword, + force_refresh: false + }) + }); + return await response.json(); +} +``` + + diff --git a/auth_doc/企业工商信息查询接口文档.md b/auth_doc/企业工商信息查询接口文档.md new file mode 100644 index 0000000..4847ccd --- /dev/null +++ b/auth_doc/企业工商信息查询接口文档.md @@ -0,0 +1,210 @@ +# 企业工商信息查询接口文档 + +## 接口概述 + +该接口用于查询企业工商注册信息,返回企业的基本信息、注册资本、经营范围、法定代表人等详细数据。 + +## 响应结构 + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| Result | Object | 查询结果数据 | +| Status | String | 状态码(200 表示成功) | +| Message | String | 响应消息 | +| OrderNumber | String | 订单编号 | + +## Result 字段说明 + +### 基本信息 + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| KeyNo | String | 100 | 主键 | +| Name | String | 1000 | 企业名称 | +| No | String | 200 | 根据企业性质返回不同值(对中国境内企业(EntType=0/1/4/7/9/10/11)返回工商注册号,对中国香港企业返回企业编号,对中国台湾企业返回企业编号) | +| BelongOrg | String | 500 | 登记机关 | +| OperId | String | 100 | 法定代表人ID | +| OperName | String | 1000 | 法定代表人名称 | +| StartDate | String | 50 | 成立日期 | +| EndDate | String | 50 | 吊销日期(保留字段) | +| Status | String | 100 | 登记状态 | +| Province | String | 32 | 省份(缩写,如 JS 表示江苏) | +| UpdatedDate | String | 50 | 更新日期 | +| CreditCode | String | 50 | 根据企业性质返回不同值(对中国境内企业(EntType=0/1/4/7/9/10/11)返回统一社会信用代码,对中国香港企业返回商业登记号码) | + +### 注册资本信息 + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| RegistCapi | String | 50 | 注册资本(含单位,如 "36225万元") | +| RegisteredCapital | String | 50 | 注册资本数额 | +| RegisteredCapitalUnit | String | 50 | 注册资本单位 | +| RegisteredCapitalCCY | String | 50 | 注册资本币种(如 CNY) | +| RecCap | String | 50 | 实缴资本(含单位) | +| PaidUpCapital | String | 50 | 实缴出资额数额 | +| PaidUpCapitalUnit | String | 50 | 实缴出资额单位 | +| PaidUpCapitalCCY | String | 50 | 实缴出资额币种 | + +### 经营信息 + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| EconKind | String | 100 | 企业类型(如 "全民所有制") | +| Address | String | 1000 | 注册地址 | +| Scope | String | 5000 | 经营范围 | +| TermStart | String | 50 | 营业期限始 | +| TermEnd | String | 50 | 营业期限至 | +| CheckDate | String | 50 | 核准日期 | +| OrgNo | String | 50 | 组织机构代码 | + +### 上市信息 + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| IsOnStock | String | 5 | 是否上市(0-未上市,1-上市) | +| StockNumber | String | 32 | 股票代码(若A股和港股同时存在,优先返回A股代码) | +| StockType | String | 10 | 上市类型(A股、港股、美股、新三板、新四板) | + +### 其他信息 + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| ImageUrl | String | 500 | 企业Logo地址 | +| EntType | String | 10 | 企业性质 | +| AreaCode | String | 20 | 行政区划代码 | + +**EntType 企业性质枚举值:** + +| 值 | 描述 | +|----|------| +| 0 | 大陆企业 | +| 1 | 社会组织 | +| 4 | 事业单位 | +| 7 | 医院 | +| 9 | 律师事务所 | +| 10 | 学校 | +| 11 | 机关单位 | +| -1 | 其他 | + +### 嵌套对象 + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| DesignatedRepresentativeList | List\ | 委派代表列表 | +| OriginalName | List\ | 曾用名列表 | +| RevokeInfo | Object | 注销吊销信息 | +| Area | Object | 行政区域 | + +--- + +## 嵌套对象字段说明 + +### DesignatedRepresentativeList(委派代表) + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| PartnerName | String | 1000 | 合伙人名称 | +| DelegatedName | String | 1000 | 委派代表名称 | + +### OriginalName(曾用名) + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| Name | String | 1000 | 曾用名 | +| ChangeDate | String | 50 | 变更日期 | + +### RevokeInfo(注销吊销信息) + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| CancelDate | String | 50 | 注销日期(如 "2022-01-01") | +| CancelReason | String | 2000 | 注销原因 | +| RevokeDate | String | 50 | 吊销日期 | +| RevokeReason | String | 2000 | 吊销原因 | + +### Area(行政区域) + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| Province | String | 32 | 省份 | +| City | String | 32 | 城市 | +| County | String | 32 | 区域 | + +--- + +## 响应示例 + +```json +{ + "Result": { + "KeyNo": "xxxxxxxxxxxx", + "Name": "xxxxxxx公司", + "No": "xxxxxxxxxxxxxxx", + "BelongOrg": "xxxxxxxxxxxx管理局", + "OperId": "xxxxxxxxxxxxx", + "OperName": "xxx", + "DesignatedRepresentativeList": [ + { + "PartnerName": "xxx股权投资有限公司", + "DelegatedName": "xxx" + } + ], + "StartDate": "1994-03-04 00:00:00", + "EndDate": "", + "Status": "注销", + "Province": "JS", + "UpdatedDate": "2021-12-17 12:34:53", + "CreditCode": "", + "RegistCapi": "36225万元", + "RegisteredCapital": "36225", + "RegisteredCapitalUnit": "万", + "RegisteredCapitalCCY": "CNY", + "EconKind": "全民所有制", + "Address": "xxx", + "Scope": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "TermStart": "1994-03-04 00:00:00", + "TermEnd": "2029-06-28 00:00:00", + "CheckDate": "2018-06-28 00:00:00", + "OrgNo": "0881xxxx-7", + "IsOnStock": "0", + "StockNumber": null, + "StockType": null, + "OriginalName": [ + { + "Name": "xxxxx有限公司", + "ChangeDate": "2023-07-06" + } + ], + "ImageUrl": "xxxxxxxxxxxxxxxxxxxxx", + "EntType": "0", + "RecCap": "36225万元", + "PaidUpCapital": "36225", + "PaidUpCapitalUnit": "万", + "PaidUpCapitalCCY": "CNY", + "RevokeInfo": { + "CancelDate": "2018-06-28", + "CancelReason": "其他原因", + "RevokeDate": "", + "RevokeReason": "" + }, + "Area": { + "Province": "江苏省", + "City": "苏州市", + "County": "苏州工业园区" + }, + "AreaCode": "320571" + }, + "Status": "200", + "Message": "【有效请求】查询成功", + "OrderNumber": "ECI2021123010294871571463" +} +``` + +--- + +## 注意事项 + +1. **日期格式**:部分日期字段返回格式为 `YYYY-MM-DD HH:mm:ss`,部分为 `YYYY-MM-DD` +2. **空值处理**:字段值可能为空字符串 `""` 或 `null`,需做兼容处理 +3. **企业性质**:根据 `EntType` 字段判断企业类型,不同类型的 `No` 和 `CreditCode` 含义不同 +4. **上市信息**:仅当 `IsOnStock` 为 `"1"` 时,`StockNumber` 和 `StockType` 才有值 diff --git a/auth_doc/失信核查接口文档.md b/auth_doc/失信核查接口文档.md new file mode 100644 index 0000000..311b18b --- /dev/null +++ b/auth_doc/失信核查接口文档.md @@ -0,0 +1,115 @@ +# 失信核查接口文档 + +## 接口概述 + +该接口用于查询企业或个人的失信被执行人信息,返回失信记录详情,包括案号、执行法院、履行情况、涉案金额等。 + +## 响应结构 + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| Paging | Object | 分页信息 | +| Result | Object | 查询结果数据 | +| Status | String | 状态码(200 表示成功) | +| Message | String | 响应消息 | +| OrderNumber | String | 订单编号 | + +## Paging(分页信息) + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| PageSize | Int32 | 每页记录数 | +| PageIndex | Int32 | 当前页码 | +| TotalRecords | Int32 | 总记录数 | + +## Result(返回参数) + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| VerifyResult | Int32 | 1 | 数据是否存在(1-存在,0-不存在) | +| Data | List\ | - | 数据信息列表 | + +**VerifyResult 枚举值:** + +| 值 | 描述 | +|----|------| +| 1 | 存在失信记录 | +| 0 | 不存在失信记录 | + +## Data(数据信息) + +| 字段名 | 类型 | 长度 | 描述 | +|--------|------|------|------| +| Id | String | 100 | 主键 | +| Liandate | String | 50 | 立案日期(如 "2022-01-01") | +| Anno | String | 1000 | 案号 | +| Executegov | String | 500 | 执行法院 | +| Executestatus | String | 5000 | 被执行人的履行情况 | +| Publicdate | String | 50 | 发布日期(如 "2022-01-01") | +| Executeno | String | 1000 | 执行依据文号 | +| ActionRemark | String | 5000 | 失信行为 | +| Amount | String | 50 | 涉案金额(元) | + +--- + +## 响应示例 + +```json +{ + "Paging": { + "PageSize": 1, + "PageIndex": 1, + "TotalRecords": 35 + }, + "Result": { + "VerifyResult": 1, + "Data": [ + { + "Id": "c2edc410f8ceed3ed256055baa8df1432", + "Liandate": "2021-08-10", + "Anno": "(2021)京0105执34224号", + "Executegov": "北京市朝阳区人民法院", + "Executestatus": "全部未履行", + "Publicdate": "2022-01-07", + "Executeno": "(2020)京0105民初51653号", + "ActionRemark": "有履行能力而拒不履行生效法律文书确定义务", + "Amount": "1098000" + } + ] + }, + "Status": "200", + "Message": "【有效请求】查询成功", + "OrderNumber": "SHIXINCHECK2022022815011713633136" +} +``` + +--- + +## 字段说明 + +### Executestatus(履行情况)常见值 + +| 值 | 描述 | +|----|------| +| 全部未履行 | 被执行人未履行任何义务 | +| 部分履行 | 被执行人已履行部分义务 | +| 全部履行 | 被执行人已全部履行义务 | + +### ActionRemark(失信行为)常见类型 + +- 有履行能力而拒不履行生效法律文书确定义务 +- 以伪造证据、暴力、威胁等方法妨碍、抗拒执行 +- 以虚假诉讼、虚假仲裁或者以隐匿、转移财产等方法规避执行 +- 违反财产报告制度 +- 违反限制消费令 +- 无正当理由拒不履行执行和解协议 + +--- + +## 注意事项 + +1. **分页处理**:当 `TotalRecords` 大于 `PageSize` 时,需要分页请求获取完整数据 +2. **金额格式**:`Amount` 字段为字符串类型,单位为元,使用时需转换为数值类型 +3. **日期格式**:日期字段格式为 `YYYY-MM-DD` +4. **空数据处理**:当 `VerifyResult` 为 `0` 时,`Data` 数组为空 +5. **案号格式**:`Anno` 和 `Executeno` 包含中文括号,注意字符编码处理