This commit is contained in:
ylj20011123 2026-02-28 18:56:16 +08:00
parent 932138e765
commit f651d3c91b
28 changed files with 714440 additions and 89 deletions

View File

@ -0,0 +1,499 @@
swaggerswagger
https://api.eshangtech.com/CommercialApi/swagger/docs/v1
api_key
Explore
CommercialApi
AbnormalAuditShow/HideList OperationsExpand Operations
post /AbnormalAudit/GetCurrentEarlyWarning
查询当日的异常稽核数据
post /AbnormalAudit/GetMonthEarlyWarning
查询月度的异常稽核数据
AnalysisShow/HideList OperationsExpand Operations
post /Analysis/GetANALYSISINSList
获取分析说明表列表
get /Analysis/GetANALYSISINSDetail
获取分析说明表明细
post /Analysis/SynchroANALYSISINS
同步分析说明表
get /Analysis/GetShopRevenue
获取门店营收数据
post /Analysis/GetShopRevenue
获取门店营收数据
get /Analysis/GetShopMerchant
获取门店商家信息
post /Analysis/GetShopMerchant
获取门店商家信息
post /Analysis/SolidTransactionAnalysis
生成时段客单交易分析数据
post /Analysis/GetTransactionAnalysis
获取时段客单交易分析数据
get /Analysis/TranslateSentence
解析语义内容中的关键字信息
get /Analysis/GetMapConfigByProvinceCode
获取地图参数配置
post /Analysis/GetServerpartTypeAnalysis
获取服务区分类定级情况
get /Analysis/verifyWXCode
解析企业微信接口验证参数
post /Analysis/verifyWXCode
解析企业微信接口验证参数
BaseInfoShow/HideList OperationsExpand Operations
get /BaseInfo/GetShopCountList
获取服务区门店商家数量列表(根据省份、服务区、区域、统计日期查询)
post /BaseInfo/GetShopCountList
获取服务区门店商家数量列表(查询条件对象)
get /BaseInfo/RecordShopCount
记录服务区门店商家数量
post /BaseInfo/RecordShopCount
记录服务区门店商家数量
get /BaseInfo/RecordProvinceShopCount
记录全省服务区门店商家数量
post /BaseInfo/RecordProvinceShopCount
记录全省服务区门店商家数量
get /BaseInfo/GetBusinessTradeList
获取经营业态列表(根据经营品牌子父级内码或者名称查询)
post /BaseInfo/GetBusinessTradeList
获取经营业态列表(查询条件对象)
get /BaseInfo/GetBrandAnalysis
服务区经营品牌分析
get /BaseInfo/GetSPRegionList
获取片区列表
get /BaseInfo/GetServerpartList
获取服务区列表
get /BaseInfo/GetServerpartInfo
获取服务区基本信息
get /BaseInfo/GetServerInfoTree
绑定区域服务区基本信息树
post /BaseInfo/GetServerpartServiceSummary
获取服务区基础设施汇总数据
post /BaseInfo/GetBrandStructureAnalysis
获取经营品牌结构分析
BigDataShow/HideList OperationsExpand Operations
get /Revenue/GetBayonetEntryList
服务区入区车流分析
get /Revenue/GetBayonetSTAList
获取车辆停留时长分析
get /Revenue/GetBayonetOAList
获取车辆归属地分析
get /Revenue/GetBayonetProvinceOAList
获取车辆省份地市归属地分析
get /Revenue/GetSPBayonetList
获取服务区车流量分析
get /Revenue/GetBayonetRankList
获取服务区车流量排行
get /Revenue/GetAvgBayonetAnalysis
获取服务区平均车流量分析
get /Revenue/GetProvinceAvgBayonetAnalysis
获取全省平均车流量分析
get /Revenue/GetBayonetSTAnalysis
获取服务区车辆时段停留时长分析
get /BigData/GetMonthAnalysis
获取月度车流分析数据
get /BigData/GetProvinceMonthAnalysis
获取全省月度车流分析数据
get /BigData/GetBayonetWarning
获取车流预警数据
get /BigData/GetHolidayBayonetWarning
获取节日车流预警数据
get /BigData/GetBayonetGrowthAnalysis
获取当日服务区车流量分析
get /BigData/GetBayonetCompare
获取服务区车流量同比分析
get /BigData/GetHolidayCompare
获取节日服务区平均入区流量对比数据
get /BigData/GetBayonetOAAnalysis
获取日均车流归属地数据分析
get /BigData/GetDateAnalysis
获取日度车流分析数据
get /BigData/CorrectBayonet
根据选择的时间范围补充卡口缺失的数据
get /BigData/JudgeBayonet
判断当前月份是否有有效车流数据
post /BigData/CorrectBayonetFlow
更新车流模拟值
get /BigData/GetBayonetOwnerAHTreeList
获取车辆归属地统计汇总列表(树形)
get /BigData/GetProvinceVehicleTreeList
获取各省入区车辆统计表(树形)
get /BigData/GetProvinceVehicleDetail
获取各省入区车辆统计表明细
get /BigData/GetBaiDuTrafficInfo
获取实况车流信息百度API
post /BigData/GetChargeStationList
获取安徽驿达服务区充电桩数据
post /BigData/SyncChargeStationToRedis
缓存安徽驿达服务区充电桩数据
post /BigData/GetCurBusyRank
获取服务区繁忙排行
post /BigData/GetRevenueTrendChart
获取今日营收趋势图
post /BigData/GetEnergyRevenueInfo
获取当日全业态营收数据
post /BigData/GetOilPriceList
获取安徽驿达服务区油价数据
BudgetShow/HideList OperationsExpand Operations
post /Budget/GetBUDGETPROJECT_AHList
获取安徽财务预算表列表
get /Budget/GetBUDGETPROJECT_AHDetail
获取安徽财务预算表明细
post /Budget/SynchroBUDGETPROJECT_AH
同步安徽财务预算表
get /Budget/DeleteBUDGETPROJECT_AH
删除安徽财务预算表
post /Budget/DeleteBUDGETPROJECT_AH
删除安徽财务预算表
get /Budget/GetBudgetProjectDetailList
获取月度安徽财务预算明细表数据
get /Budget/GetBudgetMainShow
获取安徽财务预算表明细
BusinessProcessShow/HideList OperationsExpand Operations
get /BusinessProcess/GetBusinessProcessList
获取业务审批列表
CommonShow/HideList OperationsExpand Operations
get /Common/GetDecryptString
解密字符串
get /Common/GetEncryptString
加密字符串
ContractShow/HideList OperationsExpand Operations
get /Contract/GetContractAnalysis
获取经营合同分析
get /Contract/GetMerchantAccountSplit
获取经营商户应收拆分数据
get /Contract/GetMerchantAccountDetail
获取经营商户应收拆分明细数据
CustomerShow/HideList OperationsExpand Operations
get /Customer/GetCustomerRatio
获取客群分析占比
get /Customer/GetCustomerConsumeRatio
获取客群消费能力占比
get /Customer/GetCustomerAgeRatio
获取客群年龄层次占比
get /Customer/GetCustomerGroupRatio
获取客群特征分析
get /Customer/GetAnalysisDescList
获取客群分析说明表列表
get /Customer/GetAnalysisDescDetail
获取客群分析说明表明细
get /Customer/GetCustomerSaleRatio
获取客群消费偏好数据
ExamineShow/HideList OperationsExpand Operations
post /Examine/GetEXAMINEList
获取考核管理表列表
get /Examine/GetEXAMINEDetail
获取考核管理表明细
post /Examine/GetMEETINGList
获取晨会管理表列表
get /Examine/GetMEETINGDetail
获取晨会管理表明细
post /Examine/GetPATROLList
获取日常巡检表列表
get /Examine/GetPATROLDetail
获取日常巡检表明细
get /Examine/WeChat_GetExamineList
获取小程序考核列表
get /Examine/WeChat_GetExamineDetail
获取小程序考核明细数据
get /Examine/WeChat_GetPatrolList
获取小程序日常巡检列表
get /Examine/WeChat_GetMeetingList
获取小程序晨会列表
get /Examine/GetPatrolAnalysis
获取日常巡检分析数据
get /Examine/GetExamineAnalysis
获取月度考核结果
get /Examine/GetExamineResultList
获取驿达看板-首页考核列表, 可以按照全省、片区、服务区查询结果
get /Examine/GetPatrolResultList
获取驿达看板-首页巡查列表, 可以按照全省、片区、服务区查询结果
post /Examine/GetEvaluateResList
获取考评考核数据
RevenueShow/HideList OperationsExpand Operations
get /Revenue/GetRevenuePushList
获取营收推送数据表列表
get /Revenue/GetSummaryRevenue
获取营收推送汇总数据
get /Revenue/GetSummaryRevenueMonth
获取月度营收推送汇总数据
get /Revenue/GetWechatPushSalesList
获取营收推送单品销售排行【甘肃营收推送】
get /Revenue/GetUnUpLoadShops
查询服务区未上传结账信息的门店列表
get /Revenue/GetServerpartBrand
获取服务区品牌营收
get /Revenue/GetServerpartEndAccountList
查询服务区结账数据列表
get /Revenue/GetShopEndAccountList
查询门店结账数据列表
get /Revenue/GetBudgetExpenseList
获取预算费用表列表GET
post /Revenue/GetBudgetExpenseList
获取预算费用表列表POST
get /Revenue/GetRevenueBudget
获取计划营收数据
get /Revenue/GetProvinceRevenueBudget
获取全省计划营收分析
get /Revenue/GetMobileShare
获取移动支付分账数据
get /Revenue/GetMallDeliver
获取商城配送数据
get /Revenue/GetTransactionAnalysis
获取服务区客单交易分析
get /Revenue/GetTransactionTimeAnalysis
获取服务区时段消费分析
get /Revenue/GetTransactionConvert
获取消费转化对比分析
get /Revenue/GetBusinessTradeRevenue
获取业态营收占比
get /Revenue/GetBusinessTradeLevel
获取业态消费水平占比
get /Revenue/GetBusinessBrandLevel
获取品牌消费水平占比
get /Revenue/GetRevenueCompare
获取营收同比数据
get /Revenue/GetRevenueTrend
获取营收趋势图
get /Revenue/GetRevenueReport
获取服务区经营报表
get /Revenue/GetRevenueReportDetil
获取服务区经营报表详情
get /Revenue/GetSalableCommodity
获取商超畅销商品
get /Revenue/GetSPRevenueRank
获取近日服务区营收排行
get /Revenue/GetRevenueYOY
获取每日营收同比数据
get /Revenue/GetHolidayCompare
获取节日营收同比数据
get /Revenue/GetAccountReceivable
获取营收统计明细数据
get /Revenue/GetCurRevenue
获取实时营收交易数据
get /Revenue/GetShopCurRevenue
获取实时门店营收交易数据
get /Revenue/GetLastSyncDateTime
获取最新的同步日期
get /Revenue/GetHolidayAnalysis
获取节日营收数据对比分析
get /Revenue/GetHolidayAnalysisBatch
获取多个服务区节日营收数据对比分析(批量)
get /Revenue/GetHolidaySPRAnalysis
获取节假日区域对客分析
get /Revenue/GetHolidayDailyAnalysis
获取节假日各类项目所有天数对客分析
get /Revenue/GetServerpartINCAnalysis
获取服务区营收增幅分析
get /Revenue/GetShopINCAnalysis
获取门店营收增幅分析
get /Revenue/GetMonthlyBusinessAnalysis
获取月度经营增幅分析
get /Revenue/GetMonthlySPINCAnalysis
获取服务区营收增幅分析
get /Revenue/GetMonthINCAnalysis
月度服务区门店营收对比分析
get /Revenue/GetMonthINCAnalysisSummary
汇总月度经营项目预警数值
get /Revenue/StorageMonthINCAnalysis
固化月度经营预警数据
get /Revenue/GetShopSABFIList
月度服务区门店商业适配指数SABFI
get /Revenue/GetShopMonthSABFIList
获取门店每月商业适配指数SABFI
get /Revenue/GetTransactionDetailList
获取实时交易明细
get /Revenue/GetHolidayRevenueRatio
获取节日营收占比
post /Revenue/GetBusinessRevenueList
获取云南24年经营数据分析
post /Revenue/GetMonthlyBusinessRevenue
获取云南月度经营数据分析
get /Revenue/GetCompanyRevenueReport
按照安徽驿达子公司运营的门店返回经营数据报表
SuggestionShow/HideList OperationsExpand Operations
get /Suggestion/GetMemberUnreadData
获取用户投诉建议浏览日志
get /Suggestion/RecordReadingLog
记录用户浏览投诉建议的日志
post /Suggestion/RecordReadingLog
记录用户浏览投诉建议的日志
SupplyChainShow/HideList OperationsExpand Operations
post /SupplyChain/GetMemberDashboard
获取会员总览数据统计
post /SupplyChain/GetSupplierTypeList
获取供应商分类情况
post /SupplyChain/GetSupplierList
获取供应商列表
post /SupplyChain/GetMallOrderSummary
获取线上商城统计分析数据
post /SupplyChain/GetWelFareSummary
获取员工福利汇总数据
UserBehaviorShow/HideList OperationsExpand Operations
post /UserBehavior/GetBEHAVIORRECORDList
获取用户行为记录表列表
get /UserBehavior/GetBEHAVIORRECORDDetail
获取用户行为记录表明细
post /UserBehavior/SynchroBEHAVIORRECORD
同步用户行为记录表
get /UserBehavior/AddUserBehavior
记录用户行为操作记录
[ base url: /CommercialApi , api version: v1 ]

4377
config/allApi.text Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

48839
config/api_mapping.json Normal file

File diff suppressed because it is too large Load Diff

8649
config/api_mapping.md Normal file

File diff suppressed because it is too large Load Diff

104785
config/api_mapping_merged.json Normal file

File diff suppressed because it is too large Load Diff

112348
config/api_mapping_merged.md Normal file

File diff suppressed because it is too large Load Diff

1249
config/menu.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -55,94 +55,13 @@ export default [
},
],
},
{
path: '/ahjgMenu',
name: 'ahjgMenu',
routes: [
{
path: 'busniessproject',
name: 'project.list',
component: './BussinessProject/list',
},
{
path: 'supplierList',
name: 'supplier',
component: './merchantManagement/supplier/Management/index',
},
{
path: 'servicePartAudit',
name: 'servicePartAudit',
component: './ahjgPage/servicePartAudit/index',
},
// 商品采购查询
{
path: 'shopProcurement',
name: 'shopProcurement',
component: './ahjgPage/shopProcurement/index',
},
// 调拨流程查询
{
path: 'transferProcess',
name: 'transferProcess',
component: './ahjgPage/transferProcess/index',
},
// 退货流程查询
{
path: 'returnProcess',
name: 'returnProcess',
component: './ahjgPage/returnProcess/index',
},
// 领用流程查询
{
path: 'receivingProcess',
name: 'receivingProcess',
component: './ahjgPage/receivingProcess/index',
},
// 采购入库统计表 入库退货统计表
{
path: 'purchaseReceiving',
name: 'purchaseReceiving',
component: './ahjgPage/purchaseReceiving/index',
},
// 商品退货统计表
{
path: 'productReturn',
name: 'productReturn',
component: './ahjgPage/productReturn/index',
},
// 库存信息查询
{
path: 'inventoryInformation',
name: 'inventoryInformation',
component: './ahjgPage/inventoryInformation/index',
},
// 进销存类别报表
{
path: 'InventoryCategory',
name: 'InventoryCategory',
component: './ahjgPage/InventoryCategory/index',
},
// 进销存明细报表
{
path: 'InventoryDetails',
name: 'InventoryDetails',
component: './ahjgPage/InventoryDetails/index',
},
// 一品多码进销存报表
{
path: 'oneProductMultipleSizes',
name: 'oneProductMultipleSizes',
component: './ahjgPage/oneProductMultipleSizes/index',
},
]
},
{
path: '/busniessproject',
name: 'busniessproject',
icon: 'FileProtectOutlined',
routes: [
{
// 经营合同查询
path: 'contract',
name: 'contract.list',
component: './contract/list',
@ -1422,21 +1341,25 @@ export default [
component: './merchantManagement/reports/RevenueSummary',
},
{
// 我的商品云库
path: 'commodity',
name: 'commodity',
component: './merchantManagement/commodity/BaseInfo',
},
{
// 申请商品上架
path: 'upSaleCommodity',
name: 'upSaleCommodity',
component: './merchantManagement/commodity/Update',
},
{
// 商品类别设置
path: 'category/:id',
name: 'category',
component: './merchantManagement/category',
},
{
// 在售商品查询
path: 'onsale',
name: 'onsale',
component: './merchantManagement/commodity/OnSale',
@ -1462,11 +1385,13 @@ export default [
component: './merchantManagement/Workplace/index',
},
{
// 供应商管理
path: 'supplier/list',
name: 'supplier',
component: './merchantManagement/supplier/Management/index',
},
{
// 供应商资质
path: 'supplier/qualifications',
name: 'qualifications',
component: './merchantManagement/supplier/Qualifications',
@ -1497,11 +1422,13 @@ export default [
component: './merchantManagement/assessment/assessmentSummary/index',
},
{
// 我的门店管理
path: 'shops',
name: 'shops',
component: './merchantManagement/Shops',
},
{
// 经营品牌管理
path: 'brand',
name: 'brand',
component: './merchantManagement/brand',

View File

@ -1,6 +1,6 @@
{
"name": "ant-design-pro",
"version": "4.5.82",
"version": "4.5.85",
"private": true,
"description": "An out-of-box UI solution for enterprise applications",
"scripts": {

105
scripts/add_api_names.js Normal file
View File

@ -0,0 +1,105 @@
/**
* 从多个 Swagger API 文件提取接口中文名称写入 api_mapping_merged.md
* 规则根据完整URL中的关键字匹配不同的API源文件
*/
const fs = require('fs');
const path = require('path');
// API 源配置每项包含文件名和URL中需要匹配的关键字
const API_SOURCES = [
{ file: 'allApi.text', urlKeyword: 'EShangApiMain' },
{ file: 'CommercialApiALLAPI.text', urlKeyword: 'CommercialApi' },
];
// 1. 解析所有 API 源文件,构建 { urlKeyword: { exact: {}, lower: {} } } 映射
function parseApiFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split('\n').map(l => l.trim()).filter(l => l);
const exact = {};
const lower = {};
for (let i = 0; i < lines.length - 1; i++) {
const match = lines[i].match(/^(get|post)\s+(\/\S+)$/i);
if (match) {
const apiPath = match[2];
const nextLine = lines[i + 1];
if (nextLine && !/^(get|post)\s+\//i.test(nextLine)) {
if (!exact[apiPath]) exact[apiPath] = nextLine;
const lp = apiPath.toLowerCase();
if (!lower[lp]) lower[lp] = nextLine;
}
}
}
return { exact, lower };
}
const sourceMaps = {};
for (const src of API_SOURCES) {
const filePath = path.join(__dirname, '..', 'config', src.file);
if (!fs.existsSync(filePath)) {
console.log(`⚠️ 文件不存在,跳过: ${src.file}`);
continue;
}
sourceMaps[src.urlKeyword] = parseApiFile(filePath);
console.log(`📖 ${src.file} 解析完成: ${Object.keys(sourceMaps[src.urlKeyword].exact).length} 个接口映射 (关键字: ${src.urlKeyword})`);
}
// 2. 处理 api_mapping_merged.md
const mdPath = path.join(__dirname, '..', 'config', 'api_mapping_merged.md');
const mdContent = fs.readFileSync(mdPath, 'utf-8');
const lines = mdContent.split('\n');
const newLines = [];
let addedCount = 0;
let skippedCount = 0;
let notFoundCount = 0;
for (let i = 0; i < lines.length; i++) {
newLines.push(lines[i]);
// 查找 - **接口**: `/xxx/xxx` 这一行
const interfaceMatch = lines[i].match(/^- \*\*接口\*\*:\s*`([^`]+)`/);
if (!interfaceMatch) continue;
const apiPath = interfaceMatch[1];
// 检查下一行是否已有接口名称(避免重复)
if (i + 1 < lines.length && lines[i + 1].includes('**接口名称**')) {
skippedCount++;
continue;
}
// 往后找完整URL行确定匹配哪个API源
let matchedKeyword = null;
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
if (lines[j].includes('**完整URL**')) {
for (const src of API_SOURCES) {
if (lines[j].includes(src.urlKeyword)) {
matchedKeyword = src.urlKeyword;
break;
}
}
break;
}
if (lines[j].match(/^####\s/)) break;
}
if (!matchedKeyword || !sourceMaps[matchedKeyword]) continue;
// 在对应映射中查找: 先精确匹配,再忽略大小写匹配
const maps = sourceMaps[matchedKeyword];
const chineseName = maps.exact[apiPath] || maps.lower[apiPath.toLowerCase()];
if (chineseName) {
newLines.push(`- **接口名称**: \`${chineseName}\``);
addedCount++;
} else {
notFoundCount++;
}
}
fs.writeFileSync(mdPath, newLines.join('\n'), 'utf-8');
console.log(`\n✅ 完成!`);
console.log(` 已添加接口名称: ${addedCount}`);
console.log(` 已跳过(已有名称): ${skippedCount}`);
console.log(` 未找到匹配: ${notFoundCount}`);

320
scripts/annotate_params.js Normal file
View File

@ -0,0 +1,320 @@
/**
* api_mapping_merged.md 中的请求体 JSON 字段添加中文注释
* 直接在 MD 文件上操作在每个 JSON key 后面追加 // 注释
*/
const fs = require('fs');
const path = require('path');
// 字段名 → 中文注释 映射表
const FIELD_COMMENTS = {
// ===== 通用分页/排序 =====
'pageIndex': '页码索引',
'PageIndex': '页码索引',
'pageindex': '页码索引',
'pageSize': '每页条数',
'pagesize': '每页条数',
'current': '当前页码',
'SortStr': '排序字段',
'sortstr': '排序字段',
'sortStr': '排序字段',
'keyWord': '搜索关键词',
'SearchParameter': '查询参数对象',
'searchParameter': '查询参数对象',
// ===== 关键词搜索对象 =====
'Key': '搜索字段名',
'Value': '搜索值',
'key': '搜索字段名',
'value': '搜索值',
// ===== 用户/员工信息 =====
'USER_ID': '用户ID',
'USER_NAME': '用户姓名',
'USER_PASSPORT': '用户账号',
'USER_MOBILEPHONE': '用户手机号',
'USER_LOGINIP': '登录IP地址',
'USER_LOGINPLACE': '登录地点',
'USER_STATUS': '用户状态',
'USER_PATTERN': '用户模式',
'USER_PROVINCE': '用户所属省份',
'UserTypeIds': '用户类型ID集合',
'STAFF_ID': '员工ID',
'STAFF_NAME': '员工姓名',
'Staff_ID': '员工ID',
// ===== 行为记录 =====
'BEHAVIORRECORD_ROUT': '行为记录路由',
'BEHAVIORRECORD_ROUTNAME': '行为记录路由名称',
'BEHAVIORRECORD_TIME': '行为记录时间',
'BEHAVIORRECORD_TYPE': '行为记录类型',
'BEHAVIORRECORD_TIME_Start': '行为记录开始时间',
'BEHAVIORRECORD_TIME_End': '行为记录结束时间',
// ===== 企业/商户 =====
'OWNERUNIT_ID': '业主单位ID',
'OWNERUNIT_NAME': '业主单位名称',
'OWNERUNIT_NATURE': '业主单位性质',
'OWNER_NAME': '业主名称',
'BUSINESSMAN_ID': '商户ID',
'BUSINESS_ID': '业务ID',
'PROVINCE_CODE': '省份编码',
'PROVINCE_CODES': '省份编码集合',
'OPERATE_DATE': '操作日期',
'OPERATE_DATE_Start': '操作起始日期',
'OPERATE_DATE_End': '操作结束日期',
// ===== 服务商/门店 =====
'SERVERPART_ID': '服务商ID',
'SERVERPART_IDS': '服务商ID集合',
'ServerpartIds': '服务商ID集合',
'Serverpart_IDS': '服务商ID集合',
'ServerpartId': '服务商ID',
'SERVERPARTSHOP_ID': '门店ID',
'SERVERPARTSHOP_IDS': '门店ID集合',
'ServerpartShop_ID': '门店ID',
'ServerpartShopId': '门店ID',
'SERVERPARTSTATICTYPE_ID': '门店静态类型ID',
'SHOPCODES': '门店编码集合',
'SHOPTRADE': '门店业态',
'SPREGIONTYPE_IDS': '区域类型ID集合',
// ===== 消息 =====
'RECSTAFF_ID': '接收人员工ID',
'MESSAGE_STATE': '消息状态',
'NOTICEINFO_STATE': '公告状态',
'NOTICEINFO_TYPES': '公告类型集合',
// ===== 合同相关 =====
'REGISTERCOMPACT_ID': '合同ID',
'REGISTERCOMPACT_HOSTID': '主合同ID',
'COMPACT_STATE': '合同状态',
'COMPACT_TYPE': '合同类型',
'COMPACT_STARTDATE': '合同开始日期',
'COMPACT_ENDDATE': '合同结束日期',
'COMPACT_DETAILS': '合同详情',
'AbnormalContract': '是否异常合同',
// ===== 项目相关 =====
'BUSINESSPROJECT_ID': '经营项目ID',
'BUSINESSPROJECT_IDS': '经营项目ID集合',
'BUSINESSPROJECTSPLIT_STATE': '项目拆分状态',
'PROJECT_VALID': '项目有效状态',
'PROJECT_STARTDATE': '项目开始日期',
'PROJECT_ENDDATE': '项目结束日期',
'ProjectStateSearch': '项目状态搜索',
'ProjectTypeSearch': '项目类型搜索',
'CalcAccumulate': '是否计算累计',
'ShowAccount': '是否显示账户',
'ShowRevenue': '是否显示营收',
'ShowShare': '是否显示分润',
// ===== 门店费用 =====
'SHOPEXPENSE_ID': '门店费用ID',
'SHOPEXPENSE_STATE': '门店费用状态',
'SHOPEXPENSE_TYPE': '门店费用类型',
// ===== 提成/分润 =====
'SHOPROYALTY_ID': '提成方案ID',
'ShopRoyaltyId': '提成方案ID',
'SHOPROYALTYDETAIL_STATE': '提成明细状态',
// ===== 商品相关 =====
'COMMODITY_STATE': '商品状态',
'COMMODITY_TYPE': '商品类型',
// ===== 供应商 =====
'SUPPLIER_ID': '供应商ID',
'SUPPLIER_IDS': '供应商ID集合',
'SUPPLIER_STATE': '供应商状态',
'SUPPLIEREVALUATION_ID': '供应商评价ID',
// ===== 资质 =====
'QUALIFICATION_ID': '资质ID',
'QUALIFICATION_ENDDATE': '资质到期日期',
'QUALIFICATION_STARTDATE': '资质开始日期',
'QUALIFICATION_ENDDATE_Start': '资质到期起始日期',
'QUALIFICATION_ENDDATE_End': '资质到期结束日期',
'QUALIFICATION_STATESEARCH': '资质状态搜索',
// ===== 审批流程 =====
'BusinessProcess_State': '审批状态',
'BusinessProcess_StateSearch': '审批状态搜索',
'BusinessProcess_StartDate': '审批开始日期',
'BusinessProcess_EndDate': '审批结束日期',
'PendState': '待办状态',
'Operation_Type': '操作类型',
'OPERATION_TYPES': '操作类型集合',
// ===== 销售相关 =====
'SELLER_ID': '销售人员ID',
'SELLMASTER_DATE': '销售主单日期',
'SELL_ENDDATE_Start': '销售结束起始日期',
'SELL_ENDDATE_End': '销售结束结束日期',
// ===== 财务/账单 =====
'ACCOUNT_TYPE': '账户类型',
'BillState': '账单状态',
'BILL_DATE_Start': '账单起始日期',
'BILL_DATE_End': '账单结束日期',
'SETTLEMENT_MODES': '结算方式',
// ===== 日期相关 =====
'STARTDATESearch': '开始日期搜索',
'ENDDATESearch': '结束日期搜索',
'Start_DATE': '开始日期',
'Start_Date': '开始日期',
'End_DATE': '结束日期',
'End_Date': '结束日期',
'START_DATE_Start': '开始日期(起)',
'START_DATE_End': '开始日期(止)',
'END_DATE_Start': '结束日期(起)',
'DueDate_End': '到期日期(止)',
'Due_StartDate': '到期开始日期',
'Due_EndDate': '到期结束日期',
'EFFECT_STARTDATE_End': '生效开始日期(止)',
'EFFECT_ENDDATE_Start': '生效结束日期(起)',
'dateRange': '日期范围',
'STATISTICS_DATE_Start': '统计起始日期',
'STATISTICS_DATE_End': '统计结束日期',
'STATISTICS_MONTH_Start': '统计起始月份',
'STATISTICS_MONTH_End': '统计结束月份',
'STATISTICS_TYPE': '统计类型',
// ===== 验核/审核 =====
'CHECK_STARTDATE': '审核开始日期',
'CHECK_ENDDATE': '审核结束日期',
'CHECK_STARTDATE_SEARCH': '审核开始日期搜索',
'CHECK_ENDDATE_SEARCH': '审核结束日期搜索',
'CHECK_TYPE': '审核类型',
// ===== 合作商户 =====
'COOPMERCHANTS_ID': '合作商户ID',
'COOPMERCHANTS_STATE': '合作商户状态',
'LINKER_STATE': '联系人状态',
// ===== 评价 =====
'EVALUATIONLEVEL_TYPE': '评价等级类型',
'EVALUATIONRULES_STATE': '评价规则状态',
'EVALUATION_DEPARTMENT_IDS': '评价部门ID集合',
// ===== 仓库 =====
'WAREHOUSE_STATE': '仓库状态',
// ===== 品牌/业态 =====
'BRAND_INDUSTRY': '品牌行业',
// ===== 投诉/建议 =====
'SUGGESTION_STATES': '建议状态',
'SUGGESTION_TYPES': '建议类型',
'TREATMENT_MARKSTATE': '处理标记状态',
'EXCEPTION_TYPE': '异常类型',
'ERROR_RATE': '错误率',
// ===== 投标/招商 =====
'BID_STATES': '投标状态',
// ===== 收银/支付 =====
'CASHWORKER_TYPE': '收银员类型',
'SOURCE_PLATFORMS': '来源平台集合',
// ===== 日志/操作记录 =====
'OPERATELOG_TYPES': '操作日志类型集合',
'BUSINESSLOG_TYPE': '业务日志类型',
'TABLE_NAME': '数据表名',
// ===== 其他通用 =====
'VALID': '是否有效',
'ValidState': '有效状态',
'ISVALID': '是否有效',
'ShowWholePower': '是否显示全部权限',
'HasImage': '是否有图片',
'Worker_ValId': '工作人员有效ID',
'name': '名称',
'notExist': '排除条件',
'TYPE_STATE': '类型状态',
'USERDEFINEDTYPE_STATE': '自定义类型状态',
'HOLIDAY_TYPE_IDS': '假期类型ID集合',
'ENUM_LABEL': '枚举标签',
// ===== 数据源相关参数 =====
'DataType': '数据类型',
'StartTime': '开始时间',
'EndTime': '结束时间',
'StartDate': '开始日期',
'EndDate': '结束日期',
'ServerpartShopIds': '门店ID集合',
'ExcludeOff': '是否排除关闭',
'DataSourceType': '数据来源类型',
'GroupByDaily': '是否按日分组',
'ShowRevenueSplit': '是否显示营收拆分',
'AccountDate': '账户日期',
'SearchKeyName': '搜索字段名',
'SortDesc': '是否降序',
};
// 读取 MD 文件
const mdPath = path.join(__dirname, '..', 'config', 'api_mapping_merged.md');
let content = fs.readFileSync(mdPath, 'utf-8');
// 处理 JSON 代码块中的每一行
// 匹配格式: "fieldName": "type" 或 "fieldName": {
const lines = content.split('\n');
let inJsonBlock = false;
let isDataBody = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 检测进入 JSON 代码块
if (line.trim() === '```json') {
inJsonBlock = true;
// 检查上一行是否是请求体标记
if (i > 0 && (lines[i - 1].includes('请求体 (data/body)') || lines[i - 1].includes('查询参数 (params/query)'))) {
isDataBody = true;
}
continue;
}
// 检测离开 JSON 代码块
if (line.trim() === '```') {
inJsonBlock = false;
isDataBody = false;
continue;
}
// 只处理 JSON 块内的行
if (!inJsonBlock || !isDataBody) continue;
// 跳过已有注释的行
if (line.includes('//')) continue;
// 匹配 JSON key: "fieldName": value
const match = line.match(/^(\s*)"([^"]+)":\s*(.+)$/);
if (match) {
const indent = match[1];
const fieldName = match[2];
const rest = match[3];
const comment = FIELD_COMMENTS[fieldName];
if (comment) {
// 去掉行尾逗号后添加注释,再加回逗号
const trimmedRest = rest.trimEnd();
const hasComma = trimmedRest.endsWith(',');
const valueStr = hasComma ? trimmedRest.slice(0, -1) : trimmedRest;
// 对于对象开始 { 的情况
if (valueStr.trim() === '{' || valueStr.trim() === '{}' || valueStr.trim() === '{}') {
lines[i] = `${indent}"${fieldName}": ${trimmedRest} // ${comment}`;
} else {
lines[i] = `${indent}"${fieldName}": ${valueStr}${hasComma ? ',' : ''} // ${comment}`;
}
}
}
}
content = lines.join('\n');
fs.writeFileSync(mdPath, content, 'utf-8');
// 统计注释添加数
const commentCount = (content.match(/\/\/ /g) || []).length;
console.log(`✅ 完成!共添加 ${commentCount} 处注释`);

671
scripts/extract_apis.js Normal file
View File

@ -0,0 +1,671 @@
/**
* 批量提取所有页面接口地址的自动化脚本
*
* 处理流程
* 1. 解析 menu.json 提取所有 SYSTEMMODULE_URL
* 2. 解析 routes.ts 构建嵌套路径映射子父级拼接
* 3. 定位组件文件夹及所有子组件
* 4. 提取 import service 文件
* 5. service 文件中提取 request() 路径并根据引用的 request 工具确定 prefix
* 6. 输出结果到 config/api_mapping.json config/api_mapping.md
*/
const fs = require('fs');
const path = require('path');
// 项目根目录
const ROOT = path.resolve(__dirname, '..');
const SRC = path.join(ROOT, 'src');
const PAGES = path.join(SRC, 'pages');
// ============ 第一部分request prefix 映射 ============
// 根据 service 文件 import 的 request 来源确定 prefix
const REQUEST_PREFIX_MAP = {
'request': 'https://eshangtech.com:18900/EShangApiMain',
'requestUpLoad': 'http://220.180.35.180:8000/EShangApiMain',
'requestTest': 'http://dev.eshangtech.com:8900/EShangApiMain',
'requestTestTest': 'http://dev.eshangtech.com:8001/EShangApiMain',
'requestNoPrefix': 'https://eshangtech.com:18900/',
'requestPythonTest': 'http://192.168.1.207:8002/',
'requestNewTest': 'https://eshangtech.com:18900/EShangApiMain',
'requestNewJava': 'https://java.es.eshangtech.com:443',
'requestnew': '/EShangApiDashboard',
'requestEncryption': 'https://eshangtech.com:18900/MemberApi',
'requestJava': 'https://java.es.eshangtech.com',
'requestDashboard': 'https://eshangtech.com:18900/EshangApiDashboard',
'requestCodeBuilder': 'http://dev.eshangtech.com:8001/CodeBuilderApi',
'requestCode': 'https://eshangtech.com:18900/CommercialApi',
'requestAHYD': 'https://ahyd.eshangtech.com/EShangApiMain',
};
// ============ 第二部分:解析 routes.ts 构建路径映射 ============
/**
* 解析 routes.ts 文件构建 fullPath component 映射
* 处理嵌套路由的路径拼接
*/
function parseRoutes() {
const routesFile = path.join(ROOT, 'config', 'routes.ts');
const content = fs.readFileSync(routesFile, 'utf-8');
// 用正则提取所有路由对象(简化处理:逐行匹配 path 和 component
const routeMap = {};
// 使用栈来跟踪嵌套路径
const pathStack = [];
let braceDepth = 0;
const depthToPath = {}; // 记录每个深度对应的 path 值
const lines = content.split('\n');
let currentPath = null;
let currentComponent = null;
let currentObjStartDepth = -1;
for (const line of lines) {
const trimmed = line.trim();
// 计算花括号深度
for (const ch of trimmed) {
if (ch === '{') braceDepth++;
if (ch === '}') {
// 当离开一个层级时,清除该层级的 path
if (depthToPath[braceDepth]) {
delete depthToPath[braceDepth];
}
braceDepth--;
}
}
// 匹配 path 属性
const pathMatch = trimmed.match(/^path:\s*['"]([^'"]+)['"]/);
if (pathMatch) {
const pathVal = pathMatch[1];
depthToPath[braceDepth] = pathVal;
}
// 匹配 component 属性
const compMatch = trimmed.match(/^component:\s*['"]([^'"]+)['"]/);
if (compMatch) {
const comp = compMatch[1];
// 跳过 layout 组件
if (comp.includes('Layout') || comp.includes('layout')) continue;
// 构建完整路径:从 depthToPath 中按层级拼接
const fullPath = buildFullPath(depthToPath, braceDepth);
if (fullPath && fullPath !== '/') {
routeMap[fullPath] = comp;
}
}
}
return routeMap;
}
/**
* 根据深度映射构建完整路径
*/
function buildFullPath(depthToPath, currentDepth) {
// 收集所有当前有效的路径段
const segments = [];
const depths = Object.keys(depthToPath).map(Number).sort((a, b) => a - b);
for (const d of depths) {
if (d <= currentDepth) {
segments.push(depthToPath[d]);
}
}
if (segments.length === 0) return null;
// 拼接路径
let result = '';
for (const seg of segments) {
if (seg.startsWith('/')) {
// 绝对路径,重新开始
result = seg;
} else {
// 相对路径,拼接
if (result.endsWith('/')) {
result += seg;
} else {
result += '/' + seg;
}
}
}
return result;
}
// ============ 第三部分:备用路径匹配 ============
/**
* routes.ts 中找不到完全匹配时尝试通过 URL 路径推断组件位置
* 扫描 src/pages 目录查找匹配的文件夹
*/
function findComponentByUrl(menuUrl) {
// 从 URL 提取最后一段作为组件名
const segments = menuUrl.split('/').filter(Boolean);
if (segments.length === 0) return null;
const lastSegment = segments[segments.length - 1];
// 在 pages 目录下递归搜索包含该名称的文件夹
const found = findDirRecursive(PAGES, lastSegment);
return found;
}
function findDirRecursive(dir, targetName) {
if (!fs.existsSync(dir)) return null;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
// 不区分大小写匹配
if (entry.name.toLowerCase() === targetName.toLowerCase()) {
const fullPath = path.join(dir, entry.name);
// 确认有 index.tsx 或 index.ts
if (fs.existsSync(path.join(fullPath, 'index.tsx')) ||
fs.existsSync(path.join(fullPath, 'index.ts'))) {
return fullPath;
}
}
// 递归搜索子目录
const result = findDirRecursive(path.join(dir, entry.name), targetName);
if (result) return result;
}
}
return null;
}
// ============ 第四部分:定位组件并提取所有被引用的文件 ============
/**
* 根据 component 路径解析到实际文件夹
* 处理两种情况
* 1. component 指向目录 './contract' src/pages/contract/
* 2. component 指向文件 './contract/list' src/pages/contract/list.tsx
* 此时返回文件所在的父目录
*/
function resolveComponentDir(componentPath) {
let resolved = componentPath.replace(/^\.\//, '');
const fullPath = path.join(PAGES, resolved);
// 情况1路径本身是目录如 src/pages/contract/
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
return fullPath;
}
// 情况2路径指向文件如 src/pages/contract/list → 实际是 list.tsx
// 检查是否存在同名的 .tsx / .ts 文件
const extensions = ['.tsx', '.ts', '.jsx', '.js'];
for (const ext of extensions) {
if (fs.existsSync(fullPath + ext)) {
// 文件存在,返回其父目录作为组件目录
return path.dirname(fullPath);
}
}
// 情况3尝试去掉末尾的 /index
if (resolved.endsWith('/index')) {
resolved = resolved.replace(/\/index$/, '');
const altPath = path.join(PAGES, resolved);
if (fs.existsSync(altPath) && fs.statSync(altPath).isDirectory()) {
return altPath;
}
}
return null;
}
/**
* 获取组件目录下所有 .tsx / .ts 文件包括 components 子目录
*/
function getAllComponentFiles(dir) {
const files = [];
if (!fs.existsSync(dir)) return files;
function scan(currentDir) {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
scan(fullPath);
} else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {
// 排除 service.ts 本身和 .d.ts
if (!entry.name.endsWith('.d.ts')) {
files.push(fullPath);
}
}
}
}
scan(dir);
return files;
}
// ============ 第五部分:提取 service 导入和接口路径 ============
/**
* 从组件文件中提取所有 import service 路径
*/
function extractServiceImports(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const imports = [];
// 匹配各种 import 模式
// import { xxx } from '../xxx/service'
// import { xxx } from '@/pages/xxx/service'
// import { xxx } from '@/services/xxx'
const importRegex = /import\s+\{([^}]+)\}\s+from\s+['"](.*?)['"]/g;
let match;
while ((match = importRegex.exec(content)) !== null) {
const functions = match[1].split(',').map(f => f.trim()).filter(Boolean);
let importPath = match[2];
// 只关注 service 文件、services 目录、options 目录的导入
if (importPath.includes('service') || importPath.includes('services') ||
importPath.includes('options')) {
imports.push({
functions,
importPath,
sourceFile: filePath
});
}
}
return imports;
}
/**
* 解析 import 路径到实际文件路径
*/
function resolveImportPath(importPath, sourceFile) {
let resolved;
if (importPath.startsWith('@/')) {
// @/ 别名 → src/
resolved = path.join(SRC, importPath.replace('@/', ''));
} else if (importPath.startsWith('.')) {
// 相对路径
resolved = path.resolve(path.dirname(sourceFile), importPath);
} else {
return null; // node_modules 等,跳过
}
// 尝试各种扩展名
const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx', '.js', '/index.js'];
for (const ext of extensions) {
const candidate = resolved + ext;
if (fs.existsSync(candidate)) {
return candidate;
}
}
// 如果直接存在
if (fs.existsSync(resolved)) {
if (fs.statSync(resolved).isDirectory()) {
for (const ext of ['/index.ts', '/index.tsx', '/index.js']) {
const idx = resolved + ext;
if (fs.existsSync(idx)) return idx;
}
}
return resolved;
}
return null;
}
// 缓存已解析的 service 文件
const serviceCache = {};
/**
* service 文件中提取所有 request() 调用的路径
* 同时确定引用的是哪个 request 工具确定 prefix
* 并记录每个 request() 调用所属的函数名
*/
function extractApisFromService(serviceFilePath) {
if (serviceCache[serviceFilePath]) {
return serviceCache[serviceFilePath];
}
if (!fs.existsSync(serviceFilePath)) {
serviceCache[serviceFilePath] = [];
return [];
}
const content = fs.readFileSync(serviceFilePath, 'utf-8');
const apis = [];
// 1. 确定该 service 文件使用的 request 来源
let prefix = REQUEST_PREFIX_MAP['request']; // 默认 prefix
// 匹配 import request from '@/utils/requestXXX'
const requestImportRegex = /import\s+\w+\s+from\s+['"]@\/utils\/(\w+)['"]/g;
let reqMatch;
while ((reqMatch = requestImportRegex.exec(content)) !== null) {
const requestName = reqMatch[1];
if (REQUEST_PREFIX_MAP[requestName] !== undefined) {
prefix = REQUEST_PREFIX_MAP[requestName];
}
}
// 也匹配相对路径的 request 导入
const relRequestRegex = /import\s+\w+\s+from\s+['"]\.\.?\/.*?(\w+)['"].*?/g;
while ((reqMatch = relRequestRegex.exec(content)) !== null) {
const fileName = reqMatch[1];
if (REQUEST_PREFIX_MAP[fileName] !== undefined) {
prefix = REQUEST_PREFIX_MAP[fileName];
}
}
// 2. 逐行扫描,追踪当前函数名,并提取 request() 调用
const lines = content.split('\n');
let currentFuncName = '(unknown)';
// 函数定义的正则匹配模式:
// export async function handleXxx(...)
// export function handleXxx(...)
// export const handleXxx = async (...)
// export const handleXxx = (...)
const funcDefRegex = /^export\s+(?:async\s+)?function\s+(\w+)|^export\s+const\s+(\w+)\s*=/;
for (const line of lines) {
const trimmed = line.trim();
// 检查是否是函数定义行
const funcMatch = trimmed.match(funcDefRegex);
if (funcMatch) {
currentFuncName = funcMatch[1] || funcMatch[2];
}
// 检查是否有 request() 调用
const requestCallRegex = /request\(\s*['"`]([^'"`\$]+)['"`]/g;
let apiMatch;
while ((apiMatch = requestCallRegex.exec(trimmed)) !== null) {
const apiPath = apiMatch[1];
// 构建完整 URL
let fullUrl;
if (prefix.endsWith('/')) {
fullUrl = prefix + apiPath.replace(/^\//, '');
} else {
fullUrl = prefix + apiPath;
}
apis.push({
path: apiPath,
fullUrl: fullUrl,
prefix: prefix,
functionName: currentFuncName,
serviceFile: path.relative(ROOT, serviceFilePath)
});
}
}
// 3. 也检查该 service 文件是否还 import 了其他 service二级引用
const serviceImports = extractServiceImports(serviceFilePath);
for (const imp of serviceImports) {
// 避免循环引用
const resolvedPath = resolveImportPath(imp.importPath, serviceFilePath);
if (resolvedPath && resolvedPath !== serviceFilePath && !serviceCache[resolvedPath]) {
const subApis = extractApisFromService(resolvedPath);
apis.push(...subApis);
}
}
serviceCache[serviceFilePath] = apis;
return apis;
}
// ============ 第六部分:解析 menu.json 提取所有模块 ============
function extractModules(items, parentPath = '') {
const modules = [];
for (const item of items) {
if (item.SYSTEMMODULE_URL && item.SYSTEMMODULE_URL !== '.' && item.SYSTEMMODULE_URL !== '/') {
// 跳过外部链接
if (!item.SYSTEMMODULE_URL.startsWith('http')) {
modules.push({
name: item.SYSTEMMODULE_NAME,
url: item.SYSTEMMODULE_URL,
parentMenu: parentPath
});
}
}
if (item.children) {
const menuName = item.SYSTEMMENU_NAME || '';
const newParent = parentPath ? `${parentPath} > ${menuName}` : menuName;
modules.push(...extractModules(item.children, newParent));
}
}
return modules;
}
// ============ 第七部分:主流程 ============
function main() {
console.log('🚀 开始批量提取页面接口...\n');
// 1. 读取 menu.json
const menuData = require(path.join(ROOT, 'config', 'menu.json'));
const modules = extractModules(menuData);
console.log(`📋 共发现 ${modules.length} 个有效页面模块\n`);
// 2. 解析 routes.ts 构建路径映射
const routeMap = parseRoutes();
console.log(`🗺️ 路由映射构建完成,共 ${Object.keys(routeMap).length} 条路由\n`);
// 3. 逐个处理每个模块
const results = [];
let matchedCount = 0;
let unmatchedCount = 0;
let totalApis = 0;
for (const mod of modules) {
const result = {
menuName: mod.name,
menuUrl: mod.url,
parentMenu: mod.parentMenu,
componentPath: null,
componentDir: null,
apiCount: 0,
apis: [],
status: 'unmatched'
};
// 3.1 在路由映射中查找 component
let component = routeMap[mod.url];
// 3.2 如果没有直接匹配,尝试带参数路径的匹配
if (!component) {
// 处理带动态参数的路由,比如 /setting/department/:id 匹配 /setting/department/userstype
for (const [routePath, comp] of Object.entries(routeMap)) {
if (routePath.includes(':')) {
const routeRegex = routePath.replace(/:[^/]+/g, '[^/]+');
if (new RegExp(`^${routeRegex}$`).test(mod.url)) {
component = comp;
break;
}
}
}
}
if (component) {
result.componentPath = component;
// 3.3 解析 component 路径到实际文件夹
const componentDir = resolveComponentDir(component);
if (componentDir) {
result.componentDir = path.relative(ROOT, componentDir);
result.status = 'matched';
matchedCount++;
// 3.4 获取所有组件文件
const componentFiles = getAllComponentFiles(componentDir);
// 3.5 提取所有 service 导入
const allApis = new Set(); // 用 Set 去重
const apiDetails = [];
for (const file of componentFiles) {
const serviceImports = extractServiceImports(file);
for (const imp of serviceImports) {
const resolvedService = resolveImportPath(imp.importPath, file);
if (resolvedService) {
const apis = extractApisFromService(resolvedService);
for (const api of apis) {
if (!allApis.has(api.fullUrl)) {
allApis.add(api.fullUrl);
apiDetails.push(api);
}
}
}
}
}
result.apiCount = apiDetails.length;
result.apis = apiDetails.map(a => ({
fullUrl: a.fullUrl,
path: a.path,
prefix: a.prefix,
functionName: a.functionName,
serviceFile: a.serviceFile
}));
totalApis += apiDetails.length;
} else {
result.status = 'dir_not_found';
unmatchedCount++;
}
} else {
// 3.6 备用:通过 URL 最后一段搜索
const fallbackDir = findComponentByUrl(mod.url);
if (fallbackDir) {
result.componentDir = path.relative(ROOT, fallbackDir);
result.status = 'matched_fallback';
matchedCount++;
const componentFiles = getAllComponentFiles(fallbackDir);
const allApis = new Set();
const apiDetails = [];
for (const file of componentFiles) {
const serviceImports = extractServiceImports(file);
for (const imp of serviceImports) {
const resolvedService = resolveImportPath(imp.importPath, file);
if (resolvedService) {
const apis = extractApisFromService(resolvedService);
for (const api of apis) {
if (!allApis.has(api.fullUrl)) {
allApis.add(api.fullUrl);
apiDetails.push(api);
}
}
}
}
}
result.apiCount = apiDetails.length;
result.apis = apiDetails.map(a => ({
fullUrl: a.fullUrl,
path: a.path,
prefix: a.prefix,
functionName: a.functionName,
serviceFile: a.serviceFile
}));
totalApis += apiDetails.length;
} else {
unmatchedCount++;
}
}
results.push(result);
}
// 4. 输出统计
console.log('========== 统计结果 ==========');
console.log(`✅ 匹配成功: ${matchedCount} 个页面`);
console.log(`❌ 未匹配: ${unmatchedCount} 个页面`);
console.log(`📡 总接口数: ${totalApis}`);
console.log('');
// 5. 输出 JSON 文件
const jsonOutput = path.join(ROOT, 'config', 'api_mapping.json');
fs.writeFileSync(jsonOutput, JSON.stringify(results, null, 2), 'utf-8');
console.log(`📄 JSON 已输出: ${jsonOutput}`);
// 6. 输出 Markdown 文件
const mdOutput = path.join(ROOT, 'config', 'api_mapping.md');
let md = '# 系统页面接口映射表\n\n';
md += `> 自动生成时间: ${new Date().toLocaleString()}\n\n`;
md += `> 总页面数: ${modules.length} | 匹配成功: ${matchedCount} | 未匹配: ${unmatchedCount} | 总接口数: ${totalApis}\n\n`;
// 按父级菜单分组
const groupedByParent = {};
for (const r of results) {
const parent = r.parentMenu || '未分类';
if (!groupedByParent[parent]) {
groupedByParent[parent] = [];
}
groupedByParent[parent].push(r);
}
for (const [parentMenu, items] of Object.entries(groupedByParent)) {
md += `## ${parentMenu}\n\n`;
for (const item of items) {
if (item.status === 'unmatched' || item.status === 'dir_not_found') {
md += `### ❌ ${item.menuName}\n`;
md += `- 路径: \`${item.menuUrl}\`\n`;
md += `- 状态: 未找到对应组件\n\n`;
continue;
}
md += `### ${item.menuName}\n`;
md += `- 路径: \`${item.menuUrl}\`\n`;
md += `- 组件: \`${item.componentDir || item.componentPath}\`\n`;
md += `- 调用了 **${item.apiCount}** 个接口\n\n`;
if (item.apis.length > 0) {
md += '| # | 函数名 | 接口路径 | 完整地址 | 来源 |\n';
md += '|---|--------|----------|----------|------|\n';
item.apis.forEach((api, idx) => {
md += `| ${idx + 1} | \`${api.functionName || '-'}\` | \`${api.path}\` | \`${api.fullUrl}\` | \`${api.serviceFile}\` |\n`;
});
md += '\n';
}
}
md += '---\n\n';
}
// 7. 添加未匹配页面汇总
const unmatched = results.filter(r => r.status === 'unmatched' || r.status === 'dir_not_found');
if (unmatched.length > 0) {
md += '## ⚠️ 未匹配页面汇总\n\n';
md += '| # | 页面名称 | 路径 | 所属菜单 |\n';
md += '|---|----------|------|----------|\n';
unmatched.forEach((item, idx) => {
md += `| ${idx + 1} | ${item.menuName} | \`${item.menuUrl}\` | ${item.parentMenu} |\n`;
});
md += '\n';
}
fs.writeFileSync(mdOutput, md, 'utf-8');
console.log(`📝 Markdown 已输出: ${mdOutput}`);
// 8. 打印部分结果预览
console.log('\n========== 结果预览(前 10 个有接口的页面)==========\n');
const withApis = results.filter(r => r.apiCount > 0).slice(0, 10);
for (const r of withApis) {
console.log(`📌 【${r.menuName}】调用了 ${r.apiCount} 个接口,分别为:`);
r.apis.forEach((api, idx) => {
console.log(` ${idx + 1}. ${api.fullUrl}`);
});
console.log('');
}
}
main();

View File

@ -0,0 +1,32 @@
/**
* 批量修正 apiLogger source 名称为文件名
* logApiCall(url, options, 'request') 修正为 logApiCall(url, options, '文件名')
*/
const fs = require('fs');
const path = require('path');
const utilsDir = path.join(__dirname, '..', 'src', 'utils');
const requestFiles = fs.readdirSync(utilsDir)
.filter(f => f.startsWith('request') && f.endsWith('.ts') && f !== 'request.ts');
let fixCount = 0;
for (const file of requestFiles) {
const filePath = path.join(utilsDir, file);
let content = fs.readFileSync(filePath, 'utf-8');
const sourceName = file.replace('.ts', '');
// 替换 logApiCall 中不正确的 source 名称
const oldPattern = /logApiCall\(url, options, '(\w+)'\)/;
const match = content.match(oldPattern);
if (match && match[1] !== sourceName) {
content = content.replace(oldPattern, `logApiCall(url, options, '${sourceName}')`);
fs.writeFileSync(filePath, content, 'utf-8');
console.log(`${file}: '${match[1]}' → '${sourceName}'`);
fixCount++;
} else {
console.log(`${file}: 已正确`);
}
}
console.log(`\n修正完成:${fixCount} 个文件`);

278
scripts/merge_api_data.js Normal file
View File

@ -0,0 +1,278 @@
/**
* 合并运行时 API 日志与静态 api_mapping 数据
*
* 匹配逻辑通过 URL 匹配运行时日志的 url 对应 api_mapping 中的 fullUrl
* 输出带入参结构的增强版 api_mapping
*/
const fs = require('fs');
const path = require('path');
// 读取数据
const configDir = path.join(__dirname, '..', 'config');
const apiMapping = JSON.parse(fs.readFileSync(path.join(configDir, 'api_mapping.json'), 'utf-8'));
// 自动扫描所有 api_log_*.json 文件,合并数据
const logFiles = fs.readdirSync(configDir).filter(f => f.startsWith('api_log_') && f.endsWith('.json'));
let apiLog = [];
for (const file of logFiles) {
const data = JSON.parse(fs.readFileSync(path.join(configDir, file), 'utf-8'));
console.log(` 📄 ${file}: ${data.length} 条记录`);
apiLog = apiLog.concat(data);
}
console.log(`📊 api_mapping: ${apiMapping.length} 个页面`);
console.log(`📊 api_log 总计: ${apiLog.length} 条运行时记录(来自 ${logFiles.length} 个文件)`);
// 1. 构建运行时数据索引
// 按文件时间顺序处理,新文件有数据则覆盖旧的,没数据则保留旧的
// 双重索引pageKey:url → 记录列表(按页面区分)
// 全局索引url → 记录列表(兜底用)
const pageIndex = {}; // { "页面名::URL" → [records] }
const globalIndex = {}; // { "URL" → [records] }
// 按文件名排序(文件名含时间戳,自然排序即为时间顺序)
logFiles.sort();
for (const file of logFiles) {
const data = JSON.parse(fs.readFileSync(path.join(configDir, file), 'utf-8'));
for (const record of data) {
const baseUrl = record.url.split('?')[0];
const pageName = record.currentPageName || '';
const hasData = (record.dataStructure && typeof record.dataStructure === 'object' && Object.keys(record.dataStructure).length > 0) ||
(record.paramsStructure && typeof record.paramsStructure === 'object' && Object.keys(record.paramsStructure).length > 0);
// 全局索引:新文件有数据则覆盖,无数据则追加(不覆盖已有的)
if (!globalIndex[baseUrl]) {
globalIndex[baseUrl] = [record];
} else if (hasData) {
// 新记录有入参,覆盖旧的
globalIndex[baseUrl] = [record];
}
// 新记录无入参,保留旧的,不做操作
// 页面索引
if (pageName && pageName !== '空白页') {
const pageKey = `${pageName}::${baseUrl}`;
if (!pageIndex[pageKey]) {
pageIndex[pageKey] = [record];
} else if (hasData) {
// 新记录有入参,覆盖旧的
pageIndex[pageKey] = [record];
}
// 新记录无入参,保留旧的
}
}
}
console.log(`📊 运行时唯一接口数(去参数): ${Object.keys(globalIndex).length}`);
console.log(`📊 页面级索引条目数: ${Object.keys(pageIndex).length}`);
/**
* 合并多条运行时记录的入参结构取字段并集
* 这样不同调用传了不同的可选参数都能被收集到
*/
function mergeStructures(records) {
let mergedData = null;
let mergedParams = null;
let exampleData = null;
let exampleParams = null;
let method = 'GET';
let requestType = '';
let requestSource = '';
for (const r of records) {
method = (r.method || method);
requestType = r.requestType || requestType;
requestSource = r.requestSource || requestSource;
// 合并 dataStructure
if (r.dataStructure && typeof r.dataStructure === 'object') {
if (!mergedData) mergedData = {};
deepMergeKeys(mergedData, r.dataStructure);
}
// 合并 paramsStructure
if (r.paramsStructure && typeof r.paramsStructure === 'object') {
if (!mergedParams) mergedParams = {};
deepMergeKeys(mergedParams, r.paramsStructure);
}
// 收集示例数据 (dataFull/paramsFull)
// 取最后一条有数据的记录作为示例
if (r.dataFull && Object.keys(r.dataFull).length > 0) {
exampleData = r.dataFull;
}
if (r.paramsFull && Object.keys(r.paramsFull).length > 0) {
exampleParams = r.paramsFull;
}
}
return { method, requestType, requestSource, mergedData, mergedParams, exampleData, exampleParams };
}
/**
* 深度合并 key取并集保留类型信息
*/
function deepMergeKeys(target, source) {
for (const key of Object.keys(source)) {
if (!(key in target)) {
target[key] = source[key];
} else if (typeof target[key] === 'object' && target[key] !== null &&
typeof source[key] === 'object' && source[key] !== null &&
!Array.isArray(target[key]) && !Array.isArray(source[key])) {
// 两边都是对象,递归合并
deepMergeKeys(target[key], source[key]);
}
// 如果类型不同或已有值,保留原值(先到先得)
}
}
// 2. 合并数据
let matchedApiCount = 0;
let unmatchedApiCount = 0;
let matchedPageCount = 0;
for (const page of apiMapping) {
if (!page.apis || page.apis.length === 0) continue;
let pageHasMatch = false;
const pageName = page.menuName || '';
for (const api of page.apis) {
const baseUrl = api.fullUrl.split('?')[0];
// 优先用页面级索引(精确匹配当前页面的记录)
const pageKey = `${pageName}::${baseUrl}`;
let records = pageIndex[pageKey];
// 兜底用全局索引
if (!records || records.length === 0) {
records = globalIndex[baseUrl];
}
if (records && records.length > 0) {
// 合并该页面下所有同接口调用的入参(取并集)
const merged = mergeStructures(records);
api.httpMethod = merged.method;
api.dataStructure = merged.mergedData;
api.paramsStructure = merged.mergedParams;
api.exampleData = merged.exampleData;
api.exampleParams = merged.exampleParams;
api.requestType = merged.requestType;
api.runtimeSource = merged.requestSource;
api.runtimePage = pageName;
api.hasRuntimeData = true;
// 标记是否为当前页面的精确匹配
api.exactPageMatch = !!pageIndex[pageKey];
matchedApiCount++;
pageHasMatch = true;
} else {
api.hasRuntimeData = false;
unmatchedApiCount++;
}
}
if (pageHasMatch) matchedPageCount++;
}
console.log(`\n✅ 合并结果:`);
console.log(` 已匹配接口: ${matchedApiCount}`);
console.log(` 未匹配接口: ${unmatchedApiCount}`);
console.log(` 有运行时数据的页面: ${matchedPageCount}`);
// 3. 输出合并后的 JSON
const jsonOutput = path.join(__dirname, '..', 'config', 'api_mapping_merged.json');
fs.writeFileSync(jsonOutput, JSON.stringify(apiMapping, null, 2), 'utf-8');
console.log(`\n📁 JSON 已输出: ${jsonOutput}`);
// 4. 输出 Markdown 报告
const mdOutput = path.join(__dirname, '..', 'config', 'api_mapping_merged.md');
let md = '';
md += `# API 接口映射报告(含入参)\n\n`;
md += `> 生成时间: ${new Date().toLocaleString('zh-CN')}\n\n`;
md += `## 统计\n\n`;
md += `| 指标 | 数值 |\n`;
md += `|------|------|\n`;
md += `| 总页面数 | ${apiMapping.length} |\n`;
md += `| 有运行时数据的页面 | ${matchedPageCount} |\n`;
md += `| 已匹配接口 | ${matchedApiCount} |\n`;
md += `| 未匹配接口(待采集) | ${unmatchedApiCount} |\n\n`;
// 按父菜单分组
const groups = {};
for (const item of apiMapping) {
const parent = item.parentMenu || '未分类';
if (!groups[parent]) groups[parent] = [];
groups[parent].push(item);
}
for (const [groupName, items] of Object.entries(groups)) {
md += `## ${groupName}\n\n`;
for (const item of items) {
if (item.status === 'unmatched' || item.status === 'dir_not_found') continue;
md += `### ${item.menuName}\n`;
md += `- 路径: \`${item.menuUrl}\`\n`;
md += `- 组件: \`${item.componentDir || item.componentPath}\`\n`;
md += `- 调用了 **${item.apiCount}** 个接口\n\n`;
if (item.apis && item.apis.length > 0) {
item.apis.forEach((api, idx) => {
const method = api.httpMethod || '-';
const status = api.hasRuntimeData ? '✅ 已采集' : '⏳ 待采集';
md += `#### ${idx + 1}. \`${api.functionName || '-'}\`\n\n`;
md += `- **方法**: \`${method}\`\n`;
md += `- **接口**: \`${api.path}\`\n`;
md += `- **完整URL**: \`${api.fullUrl}\`\n`;
md += `- **来源**: \`${api.serviceFile || '-'}\`\n`;
md += `- **状态**: ${status}\n`;
// 详细入参结构
if (api.dataStructure && Object.keys(api.dataStructure).length > 0) {
md += `- **请求体 (data/body)**:\n`;
md += '```json\n';
md += JSON.stringify(api.dataStructure, null, 2) + '\n';
md += '```\n';
}
if (api.paramsStructure && Object.keys(api.paramsStructure).length > 0) {
md += `- **查询参数 (params/query)**:\n`;
md += '```json\n';
md += JSON.stringify(api.paramsStructure, null, 2) + '\n';
md += '```\n';
}
// 示例请求参数展示
if ((api.exampleData && Object.keys(api.exampleData).length > 0) ||
(api.exampleParams && Object.keys(api.exampleParams).length > 0)) {
md += `- **示例请求参数 (运行时真实数据)**:\n`;
md += '```json\n';
const fullExample = {};
if (api.exampleData) fullExample.body = api.exampleData;
if (api.exampleParams) fullExample.query = api.exampleParams;
md += JSON.stringify(fullExample, null, 2) + '\n';
md += '```\n';
}
if (!api.hasRuntimeData) {
md += `- **入参**: 暂无运行时数据\n`;
} else if (
(!api.dataStructure || Object.keys(api.dataStructure).length === 0) &&
(!api.paramsStructure || Object.keys(api.paramsStructure).length === 0)
) {
md += `- **入参**: 无参数\n`;
}
md += '\n';
});
}
}
md += '---\n\n';
}
fs.writeFileSync(mdOutput, md, 'utf-8');
console.log(`📝 Markdown 已输出: ${mdOutput}`);

View File

@ -0,0 +1,18 @@
# API 映射文档自动化刷新脚本
# 顺序执行:合并 -> 参数注释 -> 接口命名
Write-Host "🚀 开始刷新 API 映射文档..." -ForegroundColor Cyan
# 1. 合并日志数据
Write-Host "`n[1/3] 正在合并运行时日志..." -ForegroundColor Yellow
node scripts/merge_api_data.js
# 2. 添加参数字段中文注释
Write-Host "`n[2/3] 正在添加参数字段注释..." -ForegroundColor Yellow
node scripts/annotate_params.js
# 3. 添加接口中文名称 (Swagger)
Write-Host "`n[3/3] 正在关联接口中文名称..." -ForegroundColor Yellow
node scripts/add_api_names.js
Write-Host "`n✅ 文档刷新完成!请查看 config/api_mapping_merged.md" -ForegroundColor Green

View File

@ -349,6 +349,10 @@ const Detail = ({ id, showTabs, tabActive, smallTabActive, isPayment, price, dat
// 拿到关联合同信息
// 拿到关联合同列表
const handleGetRelatedContracts = async (type: number, id: number) => {
if (!id) {
setRelatedContractsList([])
return
}
let req = {}
if (type === 1000) {
req = {
@ -377,7 +381,7 @@ const Detail = ({ id, showTabs, tabActive, smallTabActive, isPayment, price, dat
const list: any = []
data.data.forEach((item: any) => {
// if (type !== 1000 || (type === 1000 && item.COMPACT_DETAILS !== 1000)) {
if (item.COMPACT_DETAILS === 2000) {
if (item.COMPACT_DETAILS === 2000 || item.COMPACT_DETAILS === 1090) {
list.push(item)
}
})

View File

@ -103,6 +103,7 @@ function getMenuDom(data: ServerpartTree[], callback?: Function) {
// 项目页面主体
// const ProjecetTable: React.FC = () => {
const ProjecetTable: React.FC<{ currentUser?: CurrentUser }> = (props) => {
const serverpartObj = session.get('serverpartObj')
const { currentUser } = props
// props
// useState 为umi封装的hooks方法
@ -277,6 +278,17 @@ const ProjecetTable: React.FC<{ currentUser?: CurrentUser }> = (props) => {
},
initialValue: '1'
},
{
title: '服务区',
dataIndex: "SERVERPART_IDS",
hideInTable: true,
valueType: 'select',
valueEnum: serverpartObj,
fieldProps: {
showSearch: true, // 支持输入文字搜索
filterOption: (input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
}
},
{
title: '操作人',
dataIndex: 'STAFF_NAME',
@ -396,6 +408,7 @@ const ProjecetTable: React.FC<{ currentUser?: CurrentUser }> = (props) => {
// 发起请求return 请求返回的数据
const list = await getProjectList({
...params,
SERVERPART_IDS: params?.SERVERPART_IDS || "",
sortstr: sortstr.length ? sortstr.toString() : params?.sortstr,
keyWord: params.searchKey ? { key: "BUSINESSPROJECT_NAME,BUSINESSPROJECT_DESC,MERCHANTS_NAME,SERVERPARTSHOP_NAME", value: params.searchKey } : null, // 关键词查询
pagesize: params.pageSize,

View File

@ -308,6 +308,36 @@ const DataDictionary: React.FC<{ currentUser: CurrentUser }> = (props) => {
: []
);
};
const handleExtractModuleTree = (menuList, childKey = 'children') => {
if (!Array.isArray(menuList)) return [];
return menuList.map(menu => {
const node = {
SYSTEMMENU_ID: menu.SYSTEMMENU_ID,
SYSTEMMENU_NAME: menu.SYSTEMMENU_NAME,
children: []
};
// 1⃣ 处理子菜单(如果存在)
if (Array.isArray(menu[childKey]) && menu[childKey].length) {
node.children.push(...handleExtractModuleTree(menu[childKey], childKey));
}
// 2⃣ 处理模块 → 直接作为 children
if (Array.isArray(menu.SystemModuleList) && menu.SystemModuleList.length) {
const modules = menu.SystemModuleList.map(m => ({
SYSTEMMODULE_NAME: m.SYSTEMMODULE_NAME,
SYSTEMMODULE_URL: m.SYSTEMMODULE_URL
}));
node.children.push(...modules);
}
return node;
});
}
return (
<PageContainer header={{
title: '',
@ -327,6 +357,8 @@ const DataDictionary: React.FC<{ currentUser: CurrentUser }> = (props) => {
ShowModule: true,
SystemMenuPID: currentUser?.ID === 4653 ? '95' : '-1'
})
let res = handleExtractModuleTree(data)
console.log('data32131', res);
setMenuTree(data)
return { data, success: true }

View File

@ -1033,11 +1033,9 @@ const YearExamineProcess = ({ currentUser, onShow, setOnShow, parentRow, setPare
if (parentRow?.ENDDATE && new Date(parentRow?.ENDDATE).getTime() < new Date('2025-04-01 00:00:00').getTime()) {
list = list.filter((item: any) => item.value !== 1802);
}
}
if (currentUser?.ID === 2785) {
list.push({ label: '严琅杰', value: 2785 })
}

132
src/utils/apiLogger.ts Normal file
View File

@ -0,0 +1,132 @@
/**
* API
*
* request URL
*
*
* 使
* -
* - window.__exportApiLog() JSON
* - window.__getApiLogStats()
* - window.__clearApiLog()
*/
// 全局请求日志存储
const apiLog: any[] = [];
/**
* key 3 key
*/
const extractKeys = (obj: any, depth = 0): any => {
if (!obj || typeof obj !== 'object' || depth > 3) return typeof obj;
if (Array.isArray(obj)) {
return obj.length > 0 ? [extractKeys(obj[0], depth + 1)] : '[]';
}
const result: Record<string, any> = {};
for (const key of Object.keys(obj)) {
result[key] = extractKeys(obj[key], depth + 1);
}
return result;
};
/**
* API
* @param url URL
* @param options method, data, params
* @param requestSource 'request', 'requestCode'
*/
export function logApiCall(url: string, options: any, requestSource: string) {
try {
// 获取当前页面路径(开发模式 hash 路由)
let currentPage = '';
let currentPageName = '';
if (typeof window !== 'undefined') {
// hash 模式下从 hash 获取路径,如 #/contract/list -> /contract/list
currentPage = (window.location.hash || '').replace(/^#/, '') || window.location.pathname || '';
// 尝试从 sessionStorage 获取当前菜单名称
try {
const menuStr = sessionStorage.getItem('currentMenu') || '{}';
const currentMenu = JSON.parse(menuStr);
currentPageName = currentMenu?.name || '';
} catch (e) {
// 忽略解析错误
}
}
const record = {
url,
method: (options?.method || 'GET').toUpperCase(),
requestSource,
currentPage,
currentPageName,
timestamp: new Date().toISOString(),
// 记录完整入参(含值)
dataFull: options?.data ? JSON.parse(JSON.stringify(options.data)) : null,
paramsFull: options?.params ? JSON.parse(JSON.stringify(options.params)) : null,
// 记录入参结构(只有 key 和类型)
dataStructure: options?.data ? extractKeys(options.data) : null,
paramsStructure: options?.params ? extractKeys(options.params) : null,
requestType: options?.requestType || '',
};
apiLog.push(record);
} catch (e) {
// 记录失败不影响正常请求
console.warn('[API Logger] 记录失败:', e);
}
}
/**
* API JSON
*/
function exportApiLog() {
const dataStr = JSON.stringify(apiLog, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = `api_log_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
console.log(`[API Logger] 已导出 ${apiLog.length} 条记录`);
}
/**
*
*/
function getApiLogStats() {
const uniqueUrls = new Set(apiLog.map((r: any) => r.url));
const bySource: Record<string, number> = {};
const byPage: Record<string, number> = {};
apiLog.forEach((r: any) => {
bySource[r.requestSource] = (bySource[r.requestSource] || 0) + 1;
const pageKey = r.currentPageName || r.currentPage || '(unknown)';
byPage[pageKey] = (byPage[pageKey] || 0) + 1;
});
console.log('=== API Logger 统计 ===');
console.log(`总请求数: ${apiLog.length}`);
console.log(`唯一接口数: ${uniqueUrls.size}`);
console.log(`已覆盖页面数: ${Object.keys(byPage).length}`);
console.log('按来源统计:', bySource);
console.log('按页面统计:', byPage);
return { total: apiLog.length, unique: uniqueUrls.size, bySource, byPage };
}
/**
*
*/
function clearApiLog() {
apiLog.length = 0;
console.log('[API Logger] 日志已清空');
}
// 挂载到 window 全局对象
if (typeof window !== 'undefined') {
(window as any).__exportApiLog = exportApiLog;
(window as any).__getApiLogStats = getApiLogStats;
(window as any).__clearApiLog = clearApiLog;
(window as any).__apiLog = apiLog;
}
export default logApiCall;

View File

@ -1,4 +1,4 @@
// 由 scripts/writeVersion.js 自动生成
export const VERSION = "4.5.82";
export const GIT_HASH = "d02155b";
export const BUILD_TIME = "2026-02-12T07:10:55.081Z";
export const VERSION = "4.5.85";
export const GIT_HASH = "932138e";
export const BUILD_TIME = "2026-02-28T10:44:14.639Z";

104
walkthrough.md.resolved Normal file
View File

@ -0,0 +1,104 @@
# 历史数据回溯 — 变更检测功能开发工作日志
## 项目概述
项目路径:`e:\workfile\JAVA\AI-Python\AI-Python`
这是一个 Django + React 前后端分离项目,核心功能是从第三方 API驿达商业平台拉取数据并存储到本地数据库达梦/Oracle。项目包含
- **数据更新定时任务**`SCHEDULED_UPDATE_TASK` + `DATA_UPDATE_CONFIG`):按配置自动从第三方 API 拉取数据并 upsert 到本地表
- **历史数据回溯**`HISTORY_BACKFILL_CONFIG`):定期回溯检查历史月份数据是否有变动,按需同步
- **动态查询 API**`API_CONFIG`):根据配置动态生成本地数据查询接口
## 本次会话完成的工作
### 1. 修复本地数据获取 endpoint 错误 ✅
**问题**`_fetch_local_check_data` 使用 `DATA_UPDATE_CONFIG.NAME`(中文描述名如"更新python语义营收")构造 URL导致 404。
**修复**
- 修改 [history_backfill_service.py](file:///e:/workfile/JAVA/AI-Python/AI-Python/api/history_backfill_service.py) 的 `_get_data_update_config()` 方法,增加子查询 JOIN `API_CONFIG`,通过 `TARGET_TABLE = TABLE_NAME` 获取正确的 `local_endpoint`
- 修改 `_check_month_changed()` 使用 `duc_config['local_endpoint']` 替代 `duc_config['name']`
- 同步修复 [history_backfill_views.py](file:///e:/workfile/JAVA/AI-Python/AI-Python/api/history_backfill_views.py) 的 `BackfillCheckPreviewView`
### 2. 清理 DEBUG 日志 ✅
删除 [dynamic_query_service.py](file:///e:/workfile/JAVA/AI-Python/AI-Python/api/dynamic_query_service.py) 中所有 `[DEBUG]` 级别的 print 语句(约 30 处包括SQL 参数、字段匹配、汇总包裹、模糊查询等
### 3. 改进变更检测日志输出 ✅
修改 `_check_month_changed()` 的对比逻辑,无论字段匹配与否都输出日志:
```
[DETECT] 202501 | 对客销售 = 一致 | 本地=12345 | 远端=12345
[DETECT] 202501 | 年度累计 ≠ 不同 | 本地=99999 | 远端=88888
[DETECT] 202501 结论: 发现差异,需要全量同步
```
### 4. 调整超时时间 ✅
变更检测的 API 调用超时从 30 秒调整为 120 秒(与全量同步一致)。
### 5. MonthINCAnalysisProxyView 修改 ✅
将 `/api/revenue/month-inc-analysis/` 接口的 `ServerpartId` 从必填改为可选。
### 6. 修复 NEWGETSUMMARYREVENUEMONTH 展平问题(部分完成)
在 [data_processing_utils.py](file:///e:/workfile/JAVA/AI-Python/AI-Python/api/data_processing_utils.py) 的 `process_downloaded_data` 中,对 `NEWGETSUMMARYREVENUE` 和 `NEWGETSUMMARYREVENUEMONTH` 表跳过 `extract_serverpart_data` 步骤。
---
## 🚨 当前待解决问题
### 问题:变更检测与全量同步的数据粒度不匹配
**现象**:回溯执行"更新驿达看板首页数据缓存"NEWGETSUMMARYREVENUEMONTH 表)时,变更检测判定"有差异"后触发全量同步,但全量同步结果全部是"跳过"(数据一致),没有任何实际更新。
**根因分析**
| 阶段 | 调用方式 | 数据粒度 |
|---|---|---|
| 变更检测(本地) | `GET /api/dynamic/ahydDIBData/?StatisticsStartMonth=202501&StatisticsEndMonth=202501`(不传 ServerpartId | **省级汇总**123 条聚合成 1 条) |
| 变更检测(远端) | `GET GetSummaryRevenueMonth?pushProvinceCode=340000&StatisticsMonth=202501`(不传 ServerpartId | **省级汇总**(需确认返回结构) |
| 全量同步 | `execute_update_internal` → 按 133 个 ServerpartId 逐个调用第三方 API | **服务区级**(每次 1 条) |
**变更检测的两个子问题**
1. **远端值为 None**:之前字段路径配置错误(用 `CashPay` 而非 `MonthRevenueModel.CashPay`),用户已手动修正为嵌套路径
2. **数据口径不一致**:本地汇总是 dynamic API 的聚合计算结果,远端是第三方 API 不传 ServerpartId 时的返回值,两者可能不是同一个统计口径
**全量同步"跳过123"**
- `execute_update_internal` 调用 `process_downloaded_data` 处理数据
- upsert 按 `STATISTICS_MONTH + SERVERPART_ID` 匹配后,逐字段比对发现数据与数据库完全一致
- 这说明数据确实没变化,跳过是正确行为
- 但如果变更检测一直误判为"有差异",就会每次执行无效的全量同步(浪费资源)
---
## 关键文件说明
| 文件 | 用途 |
|---|---|
| `api/history_backfill_service.py` | 回溯服务核心:调度、变更检测、执行全量同步 |
| `api/history_backfill_views.py` | 回溯 API 视图CRUD、手动触发、检测预览 |
| `api/data_update_internal.py` | 定时任务内部执行函数(被回溯服务调用) |
| `api/data_update_views.py` | 数据更新视图(手动执行入口,有完整的行内特殊处理) |
| `api/data_processing_utils.py` | 数据处理工具展平、提取、upsert |
| `api/dynamic_query_service.py` | 动态查询服务(本地数据 API |
| `api/revenue_proxy_views.py` | 第三方 API 代理视图 |
| `frontend/src/components/BackfillConfigPage.tsx` | 回溯配置前端页面 |
## 数据库表关系
```
SCHEDULED_UPDATE_TASK (定时任务)
├── CONFIG_ID → DATA_UPDATE_CONFIG (数据源配置API URL、目标表、参数映射)
└── HISTORY_BACKFILL_CONFIG (回溯配置TASK_ID → 关联任务)
DATA_UPDATE_CONFIG.TARGET_TABLE = API_CONFIG.TABLE_NAME (1:1 或 1:N 映射)
API_CONFIG.ENDPOINT → 动态查询 API 的 endpoint 名
```
## 后续建议
1. **修正变更检测逻辑**:要么让变更检测也按服务区级别对比(但太慢),要么确认不传 ServerpartId 时第三方 API 返回的省级汇总字段路径完全正确
2. **考虑方案 B**:变更检测直接查数据库比对,绕过 HTTP 调用,避免数据结构适配问题
3. **注意 `data_update_views.py` vs `process_downloaded_data` 的差异**:前者有更完善的行内特殊处理(字段优先级保护、智能唯一键补充),后者是简化版。当前回溯使用后者