From 84009be8042bdfba27e22b093106f6bd131c2929 Mon Sep 17 00:00:00 2001 From: taocong <> Date: Mon, 5 Oct 2026 10:49:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BD=BF=E7=94=A8=E6=89=8B?= =?UTF-8?q?=E5=86=8C;=E4=BF=AE=E5=A4=8D=E6=BB=9A=E5=8A=A8=E6=9D=A1?= =?UTF-8?q?=E9=97=AE=E9=A2=98;=E6=96=B0=E5=A2=9E=E8=A1=A8=E6=98=A0?= =?UTF-8?q?=E5=B0=84=E5=85=B3=E7=B3=BB=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Uft30ChangeCode.pro | 2 + config/table_mapping.json | 812 ++++++++++++++++++ docs/使用手册.md | 56 +- docs/详细设计文档.md | 202 ++++- src/mainwindow/mainwindow.cpp | 24 +- src/mainwindow/mainwindow.h | 3 + src/pages/batchconvert/batchconvertpage.cpp | 9 +- src/pages/batchconvert/batchconvertpage.h | 2 +- src/pages/codesearch/codesearchpage.cpp | 29 +- src/pages/common/convertworkbenchbase.cpp | 34 +- src/pages/datamapping/mappingtablepage.cpp | 46 +- src/pages/datamapping/mappingtablepage.h | 12 + src/pages/datamapping/tablemappingpage.cpp | 266 +++++- src/pages/datamapping/tablemappingpage.h | 11 +- .../datamapping/twocolumnkvmappingpage.cpp | 29 + .../datamapping/twocolumnkvmappingpage.h | 6 + src/pages/functionsearch/tablesearchpage.cpp | 371 ++++++++ src/pages/functionsearch/tablesearchpage.h | 57 ++ src/pages/nonlsconvert/nonlsconvertpage.h | 2 +- src/utils/Constants.h | 7 +- src/utils/callchainanalyzer.cpp | 23 + src/utils/callchainanalyzer.h | 8 + src/utils/datacache.cpp | 79 ++ src/utils/datacache.h | 20 + src/utils/jsonfilemonitor.cpp | 12 + src/utils/jsonfilemonitor.h | 4 + 26 files changed, 2089 insertions(+), 37 deletions(-) create mode 100644 config/table_mapping.json create mode 100644 src/pages/functionsearch/tablesearchpage.cpp create mode 100644 src/pages/functionsearch/tablesearchpage.h diff --git a/Uft30ChangeCode.pro b/Uft30ChangeCode.pro index 301ba0f..d193445 100644 --- a/Uft30ChangeCode.pro +++ b/Uft30ChangeCode.pro @@ -15,6 +15,7 @@ SOURCES += main.cpp \ src/pages/functionsearch/functionsearchpagebase.cpp \ src/pages/functionsearch/uf20functionsearchpage.cpp \ src/pages/functionsearch/uft3functionsearchpage.cpp \ + src/pages/functionsearch/tablesearchpage.cpp \ src/components/elasearchedit.cpp \ src/components/interdialog.cpp \ src/pages/settings/settingspage.cpp \ @@ -60,6 +61,7 @@ HEADERS += src/mainwindow/mainwindow.h \ src/pages/functionsearch/functionsearchpagebase.h \ src/pages/functionsearch/uf20functionsearchpage.h \ src/pages/functionsearch/uft3functionsearchpage.h \ + src/pages/functionsearch/tablesearchpage.h \ src/components/elasearchedit.h \ src/components/interdialog.h \ src/pages/settings/settingspage.h \ diff --git a/config/table_mapping.json b/config/table_mapping.json new file mode 100644 index 0000000..894830f --- /dev/null +++ b/config/table_mapping.json @@ -0,0 +1,812 @@ +{ + "tableCompareUf2_Uf3": { + "accountcharacter": "account_character", + "accountconfig": "usps_account_config", + "accountdeploy": [ + "upbs_account_deploy", + "uft_account_deploy" + ], + "acctassure": "ucrt_acct_assure_code", + "acctassurearg": "acct_assure_arg", + "acctassureresult": "acct_assure_result", + "acctassurerule": "acct_assure_rule", + "acctassurescale": [ + "ucrt_acct_assure_scale", + "uarg_acct_assure_scale" + ], + "acctassurescalejour": "acct_assurescale_jour", + "acctcompactfloat": [ + "ucrt_acct_compact_float", + "uarg_acct_compact_float" + ], + "acctensurescale": [ + "ucrt_acct_ensure_scale", + "uarg_acct_ensure_scale" + ], + "acctfloatconcentratio": "acct_float_concentratio", + "acctspcash": "ucrt_acctspcash", + "acctstkconcentratio": "ucrt_acct_stk_concentrate", + "acctstkriskratio": "acct_stkrisk_ratio", + "acctststockquota": "uqms_ststock_quota_allocation", + "acctunderly": "ucrt_acct_underly_code", + "advcontract": "adviser_contract", + "advisercode": "adviser_code", + "adviserproduct": "adviser_product", + "afofagencyno": "afof_agencyno", + "afofbcrate": "afof_bcrate", + "afofbfare": "afof_bfare", + "afofcode": "afof_code", + "afofdiscount": "afof_discount", + "afofentrust": "afof_entrust", + "afofffare": "afof_ffare", + "afofquota": "afof_quota", + "afofrealtime": "afof_realtime", + "agreementctrl": "client_agreement", + "allbranch": "upbs_all_branch", + "allcompany": "upbs_all_company", + "allseats": "all_seats", + "arpacctarg": "arp_acct_arg", + "arpacctargjour": "arp_acct_arg_jour", + "arpapply": "arp_apply", + "arpapplyjour": "arp_apply_jour", + "arpassureratio": "ucbp_arp_assure_ratio", + "arpcode": "arp_code", + "arpcontract": "arp_contract", + "arpcontractjour": "arp_contract_jour", + "arpcrquota": "arp_crquota", + "arpcrquotajour": "arp_crquota_jour", + "arpequity": "arp_equity", + "arpequityjour": "arp_equity_jour", + "arpfare1": "arp_fare1", + "arpfare2": "arp_fare2", + "arpfixedrate": "arp_fixed_rate", + "arpfixedratejour": "arp_fixed_rate_jour", + "arpfunder": "arp_funder", + "arpkind": "arp_kind", + "arpmarratio": "arp_mar_ratio", + "arpquota": "arp_quota", + "arpregister": "arp_register", + "arpstkusedrate": "arp_stk_used_rate", + "assetbdstock": "ucrt_asset_bd_stock", + "assetbondjour": "bond_undue_quota", + "assetcltlimitstock": [ + "client_limit_stock", + "usms_client_limit_stock" + ], + "assetimpawnstock": "uqms_impawn_stock", + "assetriskacctlist": "asset_risk_acct_list", + "assetriskalertlist": "asset_risk_alert_list", + "assetunfinished": "unfinished", + "assunderlyarg": "assunderly_arg", + "assunderlyloginfo": "assunderly_loginfo", + "assurecode": "ucrt_assure_code", + "assurecodecp": "assure_code_cp", + "assurecodeextinfo": "assure_code_extinfo", + "assurepackage": "assure_package", + "assureprodcode": "ucrt_assure_prod_code", + "assureratio": "ucrt_assure_ratio", + "assureriskdetail": "assure_risk_detail", + "assurescale": "ucrt_assure_scale", + "assurestock": "assure_stock", + "assurestockjour": "assure_stock_jour", + "authority": "usps_authority", + "backdebtquota": "debt_quota", + "badfineinterest": "bad_fine_interest", + "badfineinterestjour": "bad_fine_interest_jour", + "bcommmodel": "bcommmodel", + "bfare1": "usps_bfare1", + "bfare1seg": "bfare1seg", + "bfare2": "usps_bfare2", + "bfare2seg": "usps_bfare2seg", + "bidcontract": "bid_contract", + "blobfile": "ucbp_blob_file", + "blockquota": "usps_block_quota", + "blpcontract": "blp_contract", + "blpcontractext": "blp_contractext", + "bondbreachinfo": "bond_breach_info", + "bondfinrate": "bond_fin_rate", + "bondimpawnarg": "bond_impawn_arg", + "bondimpcashassure": "bond_imp_cash_assure", + "bondinvestorinfo": "usps_bondinvestorinfo", + "bondlevelinfo": "usps_bond_level", + "bondputback": "bond_put_back", + "bondrate": "usps_bond_rate", + "bondrateinf": "bond_rateinf", + "bondriskarg": "bond_risk_arg", + "bondtrusteeratio": "bond_trustee_ratio", + "bprentrusttriext": "bpr_entrust_tri_ext", + "bprinfo": "uqms_bpr_info", + "branchacctinfo": "upbs_branch_acct_info", + "branchprefix": "usps_branch_prefix", + "brpcontract": "brp_contract", + "brpcontractext": "brp_contract_ext", + "brpcontractextjour": "brp_contract_ext_jour", + "brpcontractjour": "brp_contract_jour", + "btbidbookinginfo": "usps_bt_bookinginfo", + "btoaspeacct": "btoa_speacct", + "bttransinfo": "bt_transinfo", + "busiblacklist": [ + "ucrt_busi_blacklist", + "uses_busi_blacklist", + "uarg_busi_blacklist" + ], + "businflag": "upbs_business_flag", + "busiplatparam": "busi_plat_param", + "busiwhiteinfo": "business_white", + "busiwhitelist": "business_white_list", + "buypromise": "uses_buy_promise", + "cashaccount": [ + "uqms_cash_account", + "ucrt_spcash_account" + ], + "cashfund": [ + "uqms_cash_fund", + "ucrt_spcash_fund" + ], + "cashgroup": "cashgroup", + "cashquota": "ucrt_busiarg", + "cashstock": [ + "uqms_cash_stock", + "ucrt_spcash_stock" + ], + "cbpentrust": [ + "qrp_quota_total", + "ucbp_entrust" + ], + "cbpentrustbrpext": "brp_cbpentrust_ext", + "cbpentrustbtinqrspext": "cbp_entrust_bt_inqrsp_ext", + "cbpentrustbtinquiryext": "cbp_entrust_bt_inquiry_ext", + "cbpentrusttriext": "cbp_entrust_tri_ext", + "cbpetfentrustdetail": "ucbp_etf_entrust_detail", + "cbplimitsell": "ucbp_account_limit_stkcode", + "cbpprice": "usps_cbpprice", + "cbprealtime": "ucbp_realtime", + "cbpstock": "cbp_stock", + "cbsentrust": "cbs_entrust", + "cbsentrustext": "cbs_entrust_ext", + "cbsfundaccount": "uses_fund_account", + "cbsrealtime": "cbs_realtime", + "cbsstockholder": "uses_stock_holder", + "cbssubfundaccount": "uses_sub_fund_account", + "cdrfare": "usps_cdr_fare", + "cfare2": "usps_cfare2", + "cfare2seg": "usps_cfare2seg", + "chfare2": "chfare2", + "chfare2seg": "chfare2seg", + "client": [ + "uact_client", + "usms_client", + "uopt_client" + ], + "clientbondlevel": "bond_account_level", + "clienteligmatchrel": "upbs_elig_match_rel", + "clientgradeinfo": "client_grade", + "clientindivfare": "client_indivfare", + "clientindivfarejour": "client_indivfare_jour", + "clientminfare": "client_min_fare", + "clientploy": "ucrt_client_ploy", + "clientprefer": "usms_client_prefer", + "clientpreferctrl": "client_prefer", + "clientsvrfarectrl": "usps_client_svrfare", + "clientsvrfarectrljour": "usps_client_svrfare_jour", + "cmcreditquota": "cm_credit_quota", + "coffare2": "usps_coffare2", + "coffare2seg": "usps_coffare2seg", + "combentrustdetail": "uopt_combentrust_detail", + "combholdreal": "uopt_combholdreal", + "combholdrealjour": "uopt_combholdreal_jour", + "commgroup": "commgroup", + "commmodelseg": "commmodelseg", + "compact": [ + "ucrt_total_compact", + "ucrt_compact_repay_order", + "ucrt_compact", + "ucrt_split_compact" + ], + "compactapply": "ucrt_compact_apply", + "compactfare": "compactfare", + "compactfloat": "ucrt_compact_float", + "compactjour": "ucrt_compact_jour", + "compactprimerate": "ucrt_compact_primerate", + "compactrate": "ucrt_compact_rate", + "compactrealjour": "ucrt_compact_real_jour", + "confernolog": "confer_no_log", + "contract": "ucrt_contract", + "couponstkgroup": "coupon_stkgroup", + "coverstock": "uopt_covered_stock", + "coverstockjour": "uopt_coveredstock_jour", + "crdtacctextend": "ucrt_acct_extend", + "crdtacctrelationship": "crdt_acct_relationship", + "crdtaudit": "ucrt_compact_audit", + "crdtauthstkctrl": "ucrt_authority_stock", + "crdtblackcode": "ucrt_black_code", + "crdtblocktraderisk": "ucrt_blocktrade_risk", + "crdtbondputback": "ucrt_bond_putback", + "crdtbusiwhitelist": [ + "ucrt_acct_busi_control", + "crdt_busi_whitelist" + ], + "crdtclientminfare": [ + "crdt_client_min_fare", + "uarg_crt_client_minfare" + ], + "crdtclientprefer": "ucrt_client_prefer", + "crdtclientstrategyfare": "client_strategyfare", + "crdtclientstrategyfarejour": "client_strategyfare_jour", + "crdtclientsvrfare": [ + "crdt_client_svr_fare", + "uarg_crt_client_svrfare" + ], + "crdtclientsvrfarejour": "crdt_client_svrfare_jour", + "crdtcontract": "ucrt_client_primerate", + "crdtcoupon": "crdt_coupon", + "crdtcouponjour": "crdt_coupon_jour", + "crdtdictate": "ucrt_dictate", + "crdtdynafairprice": "crdt_dyna_fairprice", + "crdtentrust": [ + "ucrt_total_stk_entrust", + "ucrt_entrust", + "crtnentrust" + ], + "crdtentrustrepay": "ucrt_entrust_repay", + "crdtequity": "crdt_equity", + "crdtetfright": "ucrt_etf_right", + "crdtexemptriconbonds": [ + "ucrt_bond_exemptricon", + "uarg_crt_bond_exemptricon" + ], + "crdtffarelog": "ucrt_ffare_log", + "crdtfiles": "crdt_files", + "crdtfundaccount": "ucrt_fund_account", + "crdtgroupconcentratio": "ucrt_stk_group_concentrate", + "crdtgroupconcwhite": "ucrt_stk_group_concwhite", + "crdtlevelrate": "ucrt_level_rate", + "crdtlimitsell": "ucrt_crdt_limit_sell", + "crdtofelecagreement": "ucrt_ofelecagreement", + "crdtoutasset": "ucrt_out_asset", + "crdtoutassetapply": "ucrt_out_asset_apply", + "crdtoutassetjour": "ucrt_out_asset_jour", + "crdtpersonalizedparam": [ + "ucrt_stkcode_assure_scale", + "crdt_personalized_param" + ], + "crdtratiologinfo": "crdt_ratio_loginfo", + "crdtrealtime": "ucrt_realtime", + "crdtrestrictacct": "ucrt_acct_compact_audit", + "crdtrestrictcode": "ucrt_compact_audit_stk", + "crdtrisk": "ucrt_risk_parameter", + "crdtsettlejour": "crdt_settle_jour", + "crdtshare": "ucrt_share", + "crdtsloequityctrl": "ucrt_slo_equity", + "crdtstbipoaskresult": "ucrt_stb_ipoaskresult", + "crdtstkadfare": "crdtstkadfare", + "crdtstkholderuse": "ucrt_stock_holder", + "crdtstkresskacct": "ucrt_fundacct_stkrestrict", + "crdtstkrestrict": "ucrt_stkrestrict", + "crdtstockholder": "ucrt_stock_holder", + "crdtstockreal": "ucrt_stock_real", + "crdtstockrealjour": [ + "uqms_cash_stock_jour", + "ucrt_stock_real_jour", + "ucrt_spcash_stock_jour" + ], + "crdtstockrealtotal": "ucrt_stock_real_total", + "crdtstockrealtotaljour": "ucrt_stock_real_total_jour", + "crdtsubequity": "ucrt_subequity", + "crdttimecontrol": "ucrt_time_control", + "crdtunfinished": "ucrt_unfinished", + "crdtunityvideo": "ucrt_unity_video", + "creditquota": "credit_quotas", + "csdcacodeacct": "csdc_acode_acct", + "csdcdicttrans": "upbs_csdc_dicttrans", + "cstbfare2": "cstbfare2", + "cstbfare2seg": "cstbfare2seg", + "dataswap": "ucbp_data_swap", + "debtinterest": "usps_debtinterest", + "delistcode": "stb_delist_code", + "dfare2": "usps_dfare2", + "dfare2seg": "usps_dfare2seg", + "dictentry": "upbs_dict_entry", + "discountmodel": "usps_discount_model", + "dividendtax": [ + "ucbp_dividend_tax", + "usms_dividend_tax" + ], + "ebstkcode": "eb_stkcode", + "eligbusiarg": "upbs_elig_busi_arg", + "eligproduct": "upbs_elig_product", + "eligriskmatch": "upbs_elig_risk_match", + "eligvideoarg": "upbs_elig_video_arg", + "entrust": "uses_entrust", + "entrustwayconv": "usps_entrustway_conv", + "errormsg": "upbs_error_msg", + "etbbondcode": "etb_bond_code", + "etbbondprice": "etb_bond_price", + "etfcashbalanceinfo": "etf_cash_balanceinfo", + "etfcode": "usps_etf_code", + "etfcomponent": "usps_etf_component", + "etfdsfquotaarg": "etf_dsf_quota_arg", + "etfdsfquotaargjour": "etf_dsf_quota_arg_jour", + "etfdsfsharequota": "etf_dsf_share_quota", + "etfdsfsharequotajour": "etf_dsf_share_quota_jour", + "etfentrustdetail": "etf_entrust_detail", + "etfexchdate": "usps_etf_exchdate", + "etfrealdsfhzinfo": "etfreal_dsf_hzinfo", + "etfright": "etf_right", + "etfrightctrl": "etf_right", + "etfstock": "uses_stock_real", + "etfufundentrust": "etf_ufund_entrust", + "etfufundinfo": "etf_ufund", + "etfustockentrust": "etf_ustock_entrust", + "etfustockinfo": "etf_ustock", + "exchangedate": "upbs_init_date_model", + "exchangerate": "exchange_rates", + "exchangetime": "usps_exchange_time", + "excharg": "usps_exch_arg", + "exchboardacctdaydata": "exch_boardacct_day_data", + "exitstkcode": "usps_exit_stkcode", + "exitstock": "usms_exit_stock", + "externerror": "upbs_extern_error", + "extfarelog": [ + "ucrt_ext_fare_log", + "uses_ext_fare_log" + ], + "extstockreal": "uses_extstock_real", + "extstockrealjour": "uses_extstock_real_jour", + "fareconsult": "usps_fare_consult", + "farefundacct": "usps_farefundacct", + "faregroup": "usps_faregroup", + "faresort": "usps_faresort", + "farestandard": "usps_farestandard", + "ffare": "usps_ffare", + "ffarelog": "uses_ffare_log", + "finexeapply": "finexe_apply", + "finexeapplyjour": "finexe_apply_jour", + "finexeassurecode": "finexe_assure_code", + "finexeclarg": "finexe_clarg", + "finexecode": "finexe_code", + "finexecontract": "finexe_contract", + "finexecontractjour": "finexe_contract_jour", + "finexedictate": "finexe_dictate", + "finexeoutimpawn": "finexe_out_impawn", + "finexepreassure": "finexe_preassure", + "finexepreprodrate": "finexe_preprodrate", + "finexeprodrate": "finexe_prodrate", + "finexequota": "finexe_quota", + "finexestkquota": "finexe_stk_quota", + "finmaininfo": "fin_main_info", + "firmoffercode": "firm_offer_code", + "fixedpriceparams": "usps_fixed_price_params", + "fjystkinfo": "usps_fjy_stkinfo", + "followdaterate": "followdate_rate", + "frozendetail": "frozen_detail", + "fundaccount": [ + "usps_acct_assinfo", + "usms_fund_account", + "uopt_fund_account" + ], + "fundaccount_sleep": "fund_account_sleep", + "fundaccountctrl": "uses_fund_account", + "fundacctcomm": "fundacctcomm", + "fundbondimpawnarg": "fund_bond_impawn_arg", + "fundbusinsettlejour": "fund_busin_settle_jour", + "fundjour": "uses_fund_changeinfo", + "fundreal": [ + "ucrt_fund_real", + "ucrt_fund_detail", + "uses_fund_real", + "uses_fund_detail", + "uopt_fundreal" + ], + "fundrealjour": [ + "uqms_cash_fund_jour", + "ucrt_fund_real_jour", + "ucrt_spcash_fund_jour", + "uses_fund_real_jour", + "uopt_fundreal_jour" + ], + "fundrealtotal": "ucrt_fund_real_total", + "fundrealtotaljour": "ucrt_fund_real_total_jour", + "fundrevertjour": [ + "uqms_fund_revert_jour", + "fund_revert_jour", + "ucrt_fund_revert_jour" + ], + "fundxjour": "fund_x_jour", + "gemaccinfo": "usms_gem_acct_info", + "gmcreditquota": "gm_credit_quota", + "gradeimpawnrate": "grade_impawn_rate", + "hcommgroup": "hcommgroup", + "hcommmodel": "hcommmodel", + "hfare1": "usps_hfare1", + "hfare1seg": "hfare1seg", + "hfare2": "hfare2", + "hfare2seg": "hfare2seg", + "hkquota": "usps_hkquota", + "hkvotelist": "hkvote_list", + "hkvotestock": "hkvote_stock", + "hsfunction": "upbs_hs_function", + "hstkcodearg": "hstkcode_arg", + "hstockclientinfo": "hstock_client_info", + "hstockquotanew": "hstock_quota_new", + "hstockquotanewjour": "hstock_quota_new_jour", + "hstockquotarel": "hstock_quota_rel", + "hstockquotareljour": "hstock_quota_rel_jour", + "icsetfprodinfo": "ics_etf_prod_info", + "icsfarekind": "ics_farekind", + "icsincludefare1model": "ics_includefare1_model", + "icsprodfareratio": "ics_prodfare_ratio", + "impawnstock": "impawn_stock", + "incomeaccttraderrel": "income_accttraderrel", + "incomeagency": "usps_income_agency", + "incomepriinfo": "income_price_info", + "incomepriinfoext": "income_price_info_ext", + "incomeproduct": "income_product", + "incomepubinfo": "income_pub_info", + "incomesaler": "usps_income_saler", + "incomesalerrpt": "income_saler_declare", + "incomestkcode": "usps_income_stkcode", + "incounfininfo": "inco_unfin_info", + "incounfininfoext": "inco_unfin_info_ext", + "indexcode": "indexcode", + "indexcomponent": "index_component", + "indexconfig": "index_config", + "indivfare": "indivfare", + "initconfig": "pbs_init_config", + "intentacct": "act_intent_account", + "intentarg": "intent_arg", + "intentorgan": "intent_organ", + "iposeatcontrast": "usps_ipo_seat_contrast", + "irregularacct": "irregular_acct", + "issuemainacoderel": "issue_mainacoderel", + "issuemaingrade": "issue_main_level", + "issuemainrelinfo": "issue_main_relinfo", + "judifrozenjour": "judifrozenjour", + "licenseinfo": "upbs_license_info", + "limitclient": "limit_client", + "limitsellstkcode": "limit_sell_stkcode", + "limitsellstock": "limit_sell_stock", + "limitstockprice": "limit_stock_price", + "loginpbu": "usps_login_pbu", + "monitoracct": "monitor_acct", + "noaccountjour": "usms_noaccount_jour", + "nosettledate": "nosettle_date", + "nsriskinfo": "usps_nsrisk_info", + "ofcasharg": "of_cash_arg", + "ofcode": "usps_ofcode", + "ofcommmodel": "ofcommmodel", + "ofelecagreementctrl": "of_client_agreement", + "ofexcharg": "usps_of_exch_arg", + "offare2": "usps_offare2", + "offare2seg": "usps_offare2seg", + "ofgradectrl": "ofgradectrl", + "optacctholdlimit": "uopt_acct_holdlimit", + "optacctmarginfloat": "uopt_acct_marginfloat", + "optaidentrust": "uopt_entrustaid", + "optauthority": "uopt_authority", + "optbillcheck": "uopt_bill_check", + "optblackacctctrl": "uopt_black_acctctrl", + "optbusiconfig": "uopt_busiconfig", + "optbusiwhitelist": "uopt_busiwhitelist", + "optcashsettlement": "uopt_cash_settlement", + "optclientprefer": "uopt_client_prefer", + "optcmpacctinfo": "uopt_company_account", + "optcode": "uopt_code", + "optcombarg": "uopt_comb_arg", + "optentrust": "uopt_entrust", + "optexcharg": "uopt_excharg", + "optexchtime": "uopt_exchtime", + "optexeagreement": "uopt_exe_agreement", + "optexeassign": "uopt_exercise_assign", + "optexedeliver": "uopt_exercise_deliver", + "optfare1": "uopt_fare1", + "optfare2": "uopt_fare2", + "optfrozeninfo": "uopt_frozen_info", + "optholdlimit": "uopt_hold_limit", + "optholdreal": "uopt_holdreal", + "optholdrealassist": "uopt_holdreal_assist", + "optholdrealjour": "uopt_holdreal_jour", + "optloancompact": [ + "optloancompact", + "uopt_loancompact" + ], + "optloancompactjour": "uopt_loancompact_jour", + "optmarginfloat": "uopt_margin_float", + "optnearfinalmarmodel": "uopt_nearfinalmarmodel", + "optpersonalityrisk": "uopt_personality_risk", + "optprice": "uopt_price", + "optrates": "uopt_rates", + "optrealtime": [ + "uopt_realtime", + "uopt_realtime_sum" + ], + "optrisk": "uopt_risk", + "optstockholder": "uopt_stock_holder", + "opttradename": "uopt_trade_name", + "optunderlycode": "uopt_underly_code", + "packagetemplet": "package_template", + "payoffconfig": "payoff_config", + "payoffploy": [ + "ucrt_ploy_template", + "uarg_client_ploy" + ], + "pendfare": [ + "ucrt_pend_fare", + "ucrt_pend_fare_jour" + ], + "persecurate": "persecurate", + "persrpcode": "per_srp_code", + "persrpkind": "per_srp_kind", + "pertransbindparam": "usps_pertrans_bindparam", + "pledgestkinfo": "pledge_stkinfo", + "preassurecode": "pre_assure_code", + "prestockinfo": "pre_stock_info", + "preunderlycode": "pre_underly_code", + "prevotecode": "pre_vote_code", + "prevoteinfo": "pre_vote_info", + "price": "usps_price", + "primerateaudit": "ucrt_primerate_audit", + "primeratestr": "ucrt_primerate_str", + "productbasicinfo": "product_basic_info", + "productbasicinfoext": "product_basic_info_ext", + "promiseinfo": "usps_promise_info", + "qrpacctipwstock": "usms_qrpacct_ipwstock", + "qrpagentacct": [ + "qrp_agent_acct", + "usms_qrp_agent_acct" + ], + "qrpagentacctjour": "usms_qrp_agent_acct_jour", + "qrpbusin": "qrp_busin", + "qrpbusinjour": "qrp_busin_jour", + "qrpcode": "usps_qrp_code", + "qrpcoderegister": "qrp_code_register", + "qrpcoderegisterjour": "qrp_code_register_jour", + "qrperrinfo": "qrp_errinfo", + "qrpmultdateinfo": "qrp_mult_date_info", + "qrpptrate": "qrp_ptrate", + "qrpquota": "qrp_quota", + "qrpquotajour": "qrp_quota_jour", + "qrpunduepawn": "qrp_undue_pawn", + "quotaaudit": "quota_audit", + "realdsfbusiness": "real_dsf_business", + "realdsfmxinfo": "real_dsf_mxinfo", + "realsqwhitelist": "realsq_white_list", + "realtime": "realtime", + "reitsexpandinfo": "reits_expand_info", + "reloadfundacct": "reload_fundacct", + "reportabstract": "report_abstract", + "reptriskaccount": "ucrt_risk_account", + "reptsdsjour": "usms_sdsjour", + "reserveaccount": "usps_reserve_account", + "riskacctlist": [ + "ucrt_riskacct_list", + "uarg_riskacct_list" + ], + "riskcrdtlist": [ + "ucrt_risk_list", + "uarg_risk_list" + ], + "rsfcontract": "rsf_contract", + "rsfquota": "rsf_quota", + "rtgssettinfo": "rtgs_sett_info", + "seats": "usps_seats", + "secuauthoritystock": "uses_authority_stock", + "secubondconcratioinfo": "bond_concratio", + "secubondinfo": "secu_bond_info", + "secubondrisk": "secu_bond_risk", + "secubusictrl": "secu_busi", + "secubusictrljour": "secu_busi_jour", + "secuexemptriconbonds": "bond_exemptricon", + "secufundrealtotal": "uses_fund_real_total", + "secufundrealtotaljour": "uses_fund_real_total_jour", + "secuipoinfo": "ucbp_ipoinfo", + "secuipoinfojour": "ucbp_ipoinfojour", + "secuofacctrel": "secu_ofacct_rel", + "securate": "usps_securate", + "securisk": "secu_risk", + "secuserialcounter": "secu_serial_counter", + "secusettlejour": "secu_settle_jour", + "secusubequity": "uses_subequity", + "secuunfinished": "secu_unfinished", + "secuvotejour": "vote_jour", + "selfholder": "self_holder", + "sellfrozenstock": [ + "uref_sellfrozen_stock", + "uses_sell_frozen_stock" + ], + "shhkequity": "shhk_equity", + "shreduction": "uqms_shreduction", + "shrtgssettinfo": "sh_rtgs_sett_info", + "shsoptqryresult": "sh_sopt_qryresult", + "shstockdetail": "uqms_shstock_detail", + "slosellbalance": "ucrt_slo_sell_balance", + "slosellbalancejour": "ucrt_slo_sell_balance_jour", + "soptcode": "usps_sopt_code", + "soptentrust": "sopt_entrust", + "soptreg": [ + "uqms_sopt_reg", + "sopt_reg" + ], + "soptregjour": "uqms_sopt_reg_jour", + "sopttax": "usps_sopt_tax", + "soptwindow": "sopt_window", + "spebusindate": "usps_spe_busin_date", + "specbranchacct": "spec_branch_acct", + "spectlfundlist": "spectl_fundlist", + "spreadtype": "usps_spread_type", + "srpacctarg": "srp_acct_arg", + "srpadvarg": "srp_adv_arg", + "srpadvreg": "srp_adv_reg", + "srpapply": "srp_apply", + "srpapplyjour": "srp_apply_jour", + "srpassure": "srp_assure", + "srpassurejour": "srp_assure_jour", + "srpattentionclient": "srp_attention_client", + "srpcode": "usps_srp_code", + "srpcompact": "srp_compact", + "srpcompactjour": "srp_compact_jour", + "srpcontract": "srp_contract", + "srpcontractjour": "srp_contract_jour", + "srpcontractrate": "srp_contract_rate", + "srpequity": "srp_equity", + "srpequityjour": "srp_equity_jour", + "srpfare2": "srp_fare2", + "srpfareseg": "srp_fare2_seg", + "srpfpreclear": "srp_fpre_clear", + "srpfunder": "srp_funder", + "srpfunderquota": "srp_funder_quota", + "srpintegralinfo": "srp_integral_info", + "srpkind": "ucbp_srp_kind", + "srpmarginmode": "srp_margin_mode", + "srpmarratio": "srp_mar_ratio", + "srpquota": "srp_quota", + "srprate": "usps_srp_rate", + "srpregister": "srp_register", + "srprisklist": "srp_risk_list", + "srpwhitelist": "srp_white_list", + "sstaccount": "usms_sst_account", + "standardfare": "usps_farestandard", + "stbarg": "stb_arg", + "stbbp": "stb_bp", + "stbdlsinfo": "stb_dlsinfo", + "stbentrust": "stb_entrust", + "stbfare": "stb_fare", + "stbfare2": "usps_stbfare2", + "stbfare2seg": "usps_stbfare2seg", + "stbipoaskresult": "stb_ipo_ask_result", + "stblaycode": "usps_stb_lay_code", + "stblaycodechange": "usps_stb_laycode_change", + "stbqqcode": "stb_qqcode", + "stbstdholder": "stb_stdholder", + "stbstkcode": "usps_stb_stkcode", + "stbstockctrl": "stb_resstock", + "stbstockdetail": "uqms_stbstock_detail", + "stbstockdetailjour": "uqms_stbstock_detail_jour", + "stkadfare": "usps_stkadfare", + "stkcode": "usps_stkcode", + "stkcodeext": "usps_stkcode_ext", + "stkconcentratio": "ucrt_stk_concentrate", + "stkmodel": "usps_stkmodel", + "stknoticeinfo": "usps_stknotice_info", + "stkresone": [ + "ucrt_fundacct_stkrestrict", + "usms_crt_fundacct_stkrestrict" + ], + "stkresonectrl": "uses_fundacct_stkrestrict", + "stkresstkacct": "stkresstkacct", + "stkresstkacctctrl": "uses_fundacct_stkrestrict", + "stkrestrict": "stkrestrict", + "stkrestrictctrl": "uses_stkrestrict", + "stktradequota": "stktrade_quota", + "stktradeusedquota": "stktrade_quota_used", + "stktype": "usps_stktype", + "stock": "ucrt_stock_changeinfo", + "stockcodematch": "stock_code_match", + "stockfirm": "stock_firm", + "stockgroup": "ucrt_stk_group", + "stockholder": "usms_stock_holder", + "stockholder_sleep": "stock_holder_sleep", + "stockholderctrl": "uses_stock_holder", + "stocklockingjour": "stock_locking_jour", + "stocknet": "stock_net", + "stocknetjour": "stock_net_jour", + "stockreal": "uses_stock_real", + "stockrealjour": "uses_stock_real_jour", + "stockrealtotal": "uses_stock_real_total", + "stockrealtotaljour": "uses_stock_real_total_jour", + "stockrevertjour": [ + "uqms_stock_revert_jour", + "uses_stock_revert_jour", + "ucrt_stock_revert_jour" + ], + "stockriskratio": "stockrisk_ratio", + "strategyfare": "strategyfare", + "strategyfarecontract": "strategyfare_contract", + "strategyfarecontractjour": "strategyfare_contract_jour", + "ststockquota": "uqms_ststock_quota_used", + "subfundaccount": "uses_sub_fund_account", + "surstock": "ucrt_surplus_stock", + "surstockjour": "ucrt_surplus_stock_jour", + "svrfare": "usps_svrfare", + "sysarg": "upbs_arg", + "sysconfig": [ + "upbs_sysconfig", + "ucrt_sys_config", + "uses_sys_config", + "uopt_sys_config" + ], + "sysdeploy": "pbs_sysdeploy", + "sysdictionary": "upbs_dictionary", + "sysnode": "pbs_sysnode", + "sysnodedeploy": "pbs_sysnode_deploy", + "szhkequity": "szhk_equity", + "szreduction": "uqms_szreduction", + "szstockdetail": "uqms_szstock_detail", + "tprbasket": "tpr_basket", + "tprcontract": "tpr_contract", + "tprcontractext": [ + "tpr_contract_ext", + "tpr_contractext" + ], + "tprcontractextjour": "tpr_contract_ext_jour", + "tprcontractjour": "tpr_contract_jour", + "tprimpawnstock": "tpr_impawn_stock", + "tprimpawnstockjour": "tpr_impawn_stock_jour", + "tprspacctcontrast": "tpr_sp_acct_contrast", + "tprtransinfo": "uqms_tpr_transinfo", + "tprtransinfoext": "uqms_tpr_transinfo_ext", + "tradename": "usps_trade_name", + "traderinvestorright": "usps_trader_investor_right", + "transferboard": "transfer_board", + "transferentrust": "ucrt_transfer_entrust", + "transferfare": [ + "usps_transfer_fare", + "ucrt_transfer_fare", + "transferfare" + ], + "trasferboard": "transfer_board", + "trpttransinfo": "trpt_transinfo", + "uftaccountdeploy": "uopt_uft_account_deploy", + "uftassetbondjour": "uft_bond_undue_quota", + "uftassetimpawnstock": "uft_qms_impawn_stock", + "uftcbpstock": "uft_cbp_stock", + "uftconfig": "uft_config", + "uftetfstock": "uft_etf_stock", + "uftfundaccountsysno": "uft_fundaccount_sysno", + "uftfundnetreal": "uft_ses_fund_detail", + "uftfundreal": "uft_ses_fund_real", + "uftimpawnstock": "uft_impawn_stock", + "uftpendfare": "uft_pend_fare", + "uftshreduction": [ + "uft_qms_szreduction", + "uft_qms_shreduction" + ], + "uftstocknet": "uft_ses_stock_net", + "uftstockreal": [ + "uft_ses_stock_real", + "uft_ucrt_stock_real" + ], + "uftsubfundreal": "uft_ses_sub_fund_real", + "uftsurstock": "uft_surplus_stock", + "umt_back_acct": "umt_back_acct", + "umtquotajour": "umt_quota_jour", + "uncompact": "ucrt_uncompact", + "underlycode": "ucrt_underly_code", + "underlycodecp": "underly_code_cp", + "unityvideoctrl": "unity_video", + "unpreclear": "usms_unpreclear", + "upbs_act_rule": "upbs_acct_rule", + "upbs_asset_prop_deploy": "upbs_asset_prop_deploy", + "upbs_umtconfig": "upbs_umtconfig", + "useroperlog": "uarg_useroper_log", + "usersyncinfo": "uarg_usersyncinfo", + "usersyncinfobizarg": "uarg_usersyncinfobizarg", + "vipbranchprefix": "vipbranch_prefix", + "votecode": "usps_vote_code", + "voteinfo": "usps_vote_info", + "votelist": "usps_vote_list", + "voteresult": "vote_result", + "warrantcode": "usps_warrant_code", + "witcode": "usps_witcode" + } +} \ No newline at end of file diff --git a/docs/使用手册.md b/docs/使用手册.md index 618bb1c..fde935d 100644 --- a/docs/使用手册.md +++ b/docs/使用手册.md @@ -4,7 +4,7 @@ | --- | --- | | 软件名称 | Uft30ChangeCode(UF20 → UFT3 转码工具) | | 手册版本 | V1.6.0 | -| 更新日期 | 2026-09-14 | +| 更新日期 | 2026-10-02 | | 适用对象 | 使用本工具进行代码转码的开发/维护人员 | --- @@ -51,10 +51,14 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提 | 设置(底部) | 项目环境路径与主题配置 | | 帮助 / 关于(底部) | 在线帮助、版本信息 | +![主界面总览(功能转码 → 业务转码)](images/01_main_overview.png) + ## 5. 功能操作详解 ### 5.1 元数据更新(首次使用必做) +![元数据更新页面](images/11_metadata_update.png) + **前提**:元数据更新按"设置"页配置的项目环境路径扫描源码,请先完成 §5.5 的路径配置。 **操作步骤:** @@ -84,6 +88,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提 ##### 5.2.1.1 全量转码 +![全量转码工作台](images/01_main_overview.png) + **说明**:全量转码仅支持 **LS_ 开头**的功能("内存交易"改造范围),按清单对功能进行整体转码。清单保存在 `cust.json` 的 `convert.dir` 节点(见 §6.1)。 **表格列**:`UF20函数名` | `是否存在` | `核心` | `是否柜面接口`(**双击函数名所在行**可在弹窗中修改函数名)。 @@ -105,6 +111,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提 ##### 5.2.1.2 按需转码 +![按需转码工作台](images/03_ondemand_convert.png) + **说明**:按需转码针对**非 LS 功能**转码,支持**单文件转码**与**递归转码**两种模式,转码范围不包含核心文件、module 模块文件。 - **非递归模式**:仅对目标文件本身执行转码,不会处理该文件内调用的函数; @@ -132,6 +140,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提 #### 5.2.2 代码检索 +![代码检索页面(转码业务模式)](images/04_code_search.png) + **说明**:代码检索用于查看功能的**调用链路**——该功能调用了哪些下游函数、被哪些上游功能调用,用于转码前评估影响范围、转码后核对结果。 页面顶部 Tab 切换 **转码业务** / **非转码业务** 两种模式,两种模式各自维护检索路径与索引,互不干扰。 @@ -168,6 +178,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提 #### 5.3.1 UF20 功能查询 +![UF20 功能查询页面(批量关键词:证券)](images/05_uf20_function_search.png) + **操作步骤:** 1. 进入 **UF20功能查询** 页; @@ -184,16 +196,42 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提 #### 5.3.2 UFT3 功能查询 +![UFT3 功能查询页面(关键词:fund)](images/06_uft3_function_search.png) + - 界面与 UF20 功能查询一致(函数名 / 功能号检索、**批量关键词**、精确匹配),结果列为 `函数名`、`英文名`、`功能号`,查询提示同样在页面底部提示栏反馈; - 仅用于查询核对 UFT3 功能元数据,**双击/右键不加入转码清单**; - 代码检索中"原代码已支持"行前的功能号即来自 UFT3 元数据。 +#### 5.3.3 UF20 & 内存表映射查询 + +![UF20 & 内存表映射查询页面(模糊查询:acct)](images/07_table_mapping_search.png) + +检索 UF20(2.0)表与内存表之间的映射关系,结果四列:`UF20表名`、`UF20表中文名`、`内存表名`、`内存表中文名`;**双击结果行可添加映射到「数据映射 → 表替换」**。 + +**操作步骤:** + +1. 进入 **功能查询 → UF20 & 内存表映射查询**; +2. 输入 **UF20 表名或内存表名**(二者均可检索),回车或点搜索图标; + - **批量查询**:多个表名用**逗号、分号或空格**分隔(中英文均可),各关键词命中结果取并集展示; + - 搜索框内可切换**精确匹配**(默认模糊匹配,不区分大小写); +3. 查看结果列表: + - 映射关系命中(表名在映射文件 `bin/uf2touft3/config/table_mapping.json` 中配置过)→ 四列完整展示,中文名分别取自 `uf20_cache.db` / `uft3_cache.db`;一个 UF20 表映射到多个内存表(value 为数组)时展示为多条记录; + - 无映射关系 → 仅展示数据库中能取到的一侧字段,映射相关列置空,底部提示注明无映射数量; +4. **双击结果行**将该映射关系添加到「数据映射 → 表替换」: + - 仅当该行 UF20 表名与内存表名均非空(有完整映射关系)才可添加; + - 表替换中已存在该 UF20 表名时自动跳过,并在提示栏注明当前映射值(不覆盖); + - 添加成功后**自动保存**到 `cust.json`(提示栏回执"已添加并自动保存",无需再到表替换页手动保存),同时同步追加到映射文件 `table_mapping.json` 中缺失的条目,**再次查询立即可见**。 + +**查询提示**(页面底部提示栏,不弹窗):默认提示"💡 输入 UF20 表名或内存表名进行查询,支持精确/模糊匹配;双击记录行可添加到「数据映射 → 表替换」";无命中提示"⚠️ 未找到匹配的表";缓存库无表数据提示"请先在「元数据更新」页面执行更新";**映射文件解析失败**(手工编辑 `table_mapping.json` 留下尾随逗号、缺引号等)提示"⚠️ 映射文件 table_mapping.json 解析失败(第 N 行附近)…"并停止查询(不再单侧展示,避免误判为映射丢失),按行号修正文件后重新查询即可;成功显示"已查询 X 条记录(其中 Y 个无映射关系,N 个表中文名缺失,M 个关键词无匹配)"——一对多映射按多条记录计;中文名缺失表示映射关系里有该表、但本地缓存库未收录(元数据更新未覆盖该表来源工程),需扩大元数据更新的工程范围。 + ### 5.4 数据映射 **说明**:数据映射维护 UF20 → UFT3 的**替换对照表**与**不转码清单**,转码引擎按映射替换代码中的表名 / 函数名,清单内的功能函数名则跳过替换。各页数据均保存在 `cust.json`(见 §6.1);界面修改后点 **保存** 写盘生效,外部编辑 `cust.json` 保存后界面自动刷新(程序自身保存后的 2 秒内暂停文件监控,此间的外部修改需再次保存文件才会被检测到)。 #### 5.4.1 表替换 +![表替换页面](images/08_table_replace.png) + 维护 UF20 表名 → UFT3 表名的映射。 **操作步骤:** @@ -203,14 +241,22 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提 3. **双击单元格**直接修改; 4. 点 **添加** 弹窗输入 UF20 / UFT3 表名(UF20 表名必填且不可重复); 5. **删除选中** 需二次确认(支持多选); -6. 修改后点 **保存** 生效。 +6. 修改后点 **保存** 生效;保存成功后自动与映射文件 `bin/uf2touft3/config/table_mapping.json` 对账同步,「UF20 & 内存表映射查询」中**立即可查**: + - **新增**的映射自动追加到该文件; + - **修改一对一映射的内存表名**后,文件中同名条目的值同步更新; + - 该文件中配置为**一对多数组**的条目(一个 UF20 表对应多个内存表)保持不变,不会被表替换中的单值覆盖; + - 在表替换中**删除**条目暂不同步到该文件(映射文件是全量参考库,删除仅表示转码时不再使用该替换)。 #### 5.4.2 函数替换 -维护 UF20 函数名 → UFT3 函数名的映射,操作方式与表替换完全相同。 +![函数替换页面](images/09_function_replace.png) + +维护 UF20 函数名 → UFT3 函数名的映射,操作方式与表替换相同,但**不涉及** `table_mapping.json` 自动同步(该同步为表替换独有)。 #### 5.4.3 不转码功能函数 +![不转码功能函数页面](images/10_nochange_fullname.png) + 维护**不参与函数名替换**的功能函数清单,对应 `cust.json` 顶层键 `noChangeFullNameCode`(字符串数组)。转码时,清单内的功能函数名保持原样,不做 UF20 → UFT3 全名替换。 **操作步骤:** @@ -226,6 +272,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提 ### 5.5 系统设置(底部"设置") +![系统设置页面](images/12_settings.png) + **环境配置**: | 配置项 | 说明 | @@ -264,7 +312,7 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提 | `functionReplace` | 对象 `{UF20函数名: UFT3函数名}` | 函数替换映射 | | `noChangeFullNameCode` | 字符串数组 | 不转码功能函数清单,转码时跳过这些函数名的替换 | -`convert.dir` 元素字段说明(详见 `docs/转码配置格式契约.md`): +`convert.dir` 元素字段说明(详见 `docs/详细设计文档.md` §7.5): | 字段 | 说明 | | --- | --- | diff --git a/docs/详细设计文档.md b/docs/详细设计文档.md index 240d722..29bfdde 100644 --- a/docs/详细设计文档.md +++ b/docs/详细设计文档.md @@ -317,7 +317,7 @@ sequenceDiagram **要点:** -- **全量转码**:引擎读取 `cust.json` 的 `convert.dir` 清单(V2 对象数组 `{name, core, viaUmgr, dialect}`),对清单内 LS_ 功能整体转码,核心/柜面接口/SQL 方言随清单下发(契约见 `docs/转码配置格式契约.md`); +- **全量转码**:引擎读取 `cust.json` 的 `convert.dir` 清单(V2 对象数组 `{name, core, viaUmgr, dialect}`),对清单内 LS_ 功能整体转码,核心/柜面接口/SQL 方言随清单下发(契约见 §7.5); - **按需转码**:`NonLsConvertPage::buildArguments()` 组装引擎参数(含 `-m 2` 模式标识),功能清单、递归标志(depth)与 SQL 方言(dialect)由 `cust_nonls.json` 提供,引擎按映射关系(`cust.json`)完成表名/函数名替换; - **成败判定**:引擎在个别文件转码失败时退出码仍可能为 0,因此 `MainWindow` 在输出解析链最前端检测 `FAIL` 行(`[N/M] FAIL 函数名`)与汇总行(`批量转码完成: X/Y 通过, Z 失败`)累计 `m_failCount`;收尾按 `正常退出 && 退出码==0 && m_failCount==0` 判定成功,失败时页面调用 `onConvertFailed`——进度条**保持当前进度**(不置 100%)、状态文字"转换失败(N 个文件转换失败)"(详见 §8.7); - **转码后联动**:任务成功后自动静默刷新代码检索索引,保证调用链路反映最新代码。 @@ -461,7 +461,7 @@ flowchart TD 两种模式均**不包含核心文件、module 模块文件**。执行时 `buildArguments()` 组装引擎参数(含 `-m 2`),由 `MainWindow` 统一经 `PythonRunner` 启动引擎。 -### 5.4 功能查询模块(FunctionSearchPage 基类 + UF20/UFT3 子页) +### 5.4 功能查询模块(FunctionSearchPage 基类 + UF20/UFT3 子页 + UF20 & 内存表映射查询页) **职责**:检索 UF20 / UFT3 功能元数据(依赖 `DataCache`),列:`函数名 / 英文名 / 功能号`;支持函数名/功能号字段切换、精确匹配开关与**多关键词批量查询**。UF20 页结果表支持多选,双击/右键一键加入转码工作台;UFT3 页为纯查询(不接入转码链路)。 @@ -504,6 +504,63 @@ sequenceDiagram > **UFT3 功能查询**:界面与 UF20 版一致(含批量关键词查询),仅用于核对 UFT3 元数据,**双击/右键不加入转码清单**;代码检索中"原代码已支持"行的功能号前缀即来自 UFT3 元数据。 +#### 5.4.3 UF20 & 内存表映射查询页(TableSearchPage) + +**职责**:检索 UF20(2.0)表与内存表(UFT3)之间的映射关系,四列结果:`UF20表名 / UF20表中文名 / 内存表名 / 内存表中文名`;**双击记录行可添加映射到「数据映射 → 表替换」页**。不继承 `FunctionSearchPageBase`(结果结构为 4 列表信息,与 `FunctionInfo` 3 列函数信息不同),UI 风格(边距/搜索框/结果表/主题感知 tip)与功能查询页对齐。 + +**数据源三件套**: + +| 列 | 来源 | +| --- | --- | +| UF20表名 / 内存表名 | `bin/uf2touft3/config/table_mapping.json`(独立映射文件,顶层键 `tableCompareUf2_Uf3`:key=UF20表名,value=内存表名;value 可为字符串(一对一)或字符串数组(一对多,拆为多条映射对,一个 UF20 表展示多行)),每次查询现读(文件小) | +| UF20表中文名 | `uf20_cache.db` 的 `uf2_table`:`data` JSON 的 `cname` 字段 | +| 内存表中文名 | `uft3_cache.db` 的 `uft3_table`:`data` JSON 的 `chineseName` 字段 | + +**查询流程**(每关键词,支持多关键词 OR 并集,`splitKeywords` 与功能查询页同口径): + +1. **映射命中**(key 或 value)→ 完整行:以 key 取 UF20 中文名、以 value 取内存表中文名;一对多映射(value 为数组)拆为多条记录展示; +2. **映射未命中** → 双库单侧探测:命中 `uf2_table` 出单侧行(映射两列置空),命中 `uft3_table` 同理,两侧同时命中出两行; +3. **去重**:映射整体行已覆盖的表名不再补单侧行(covered 集合),同一映射对不被多关键词重复输出(seenPairs 集合); +4. 精确匹配为全等;模糊匹配为不区分大小写的包含。 + +**轻量数据访问**:两库 `data` 列为完整表定义 JSON(单行可达数十 KB),禁止整表搬运逐行解析——`DataCache` 新增 `getUf20TableNameMap()` / `getUft3TableNameMap()`,SQL 层 `json_extract(data, '$.cname' / '$.chineseName')` 只提取中文名,首次调用全量构建"表名→中文名"映射后**内存缓存**(懒加载,表数量远小于接口表,秒级内完成),元数据落库后随 `refreshFunctionCache()` 一并失效重建;构建失败不置有效、下次自动重试;`json_extract` 不可用时回退 `getAllRows` + C++ 逐行解析兜底。 + +**tip 反馈**(页面底部,不弹窗):默认提示"💡 输入 UF20 表名或内存表名进行查询,支持精确/模糊匹配;双击记录行可添加到「数据映射 → 表替换」";空输入提示输入表名;双库均为空提示"请先在「元数据更新」页面执行更新";无命中提示"未找到匹配的表";成功显示"已查询 X 条记录(其中 Y 个无映射关系,N 个表中文名缺失,M 个关键词无匹配)"——一对多映射按多条记录计;中文名缺失指映射里有该表但缓存库未收录(元数据更新未覆盖其来源工程,如 `table_mapping.json` 覆盖完整标准套件而本地仅更新了部分工程);`table_mapping.json` 读取失败时附加降级说明(仅展示数据库检索结果)。 + +#### 5.4.4 双击添加映射到表替换页 + +查询结果行**双击**即可将映射关系添加到「数据映射 → 表替换」页,跨页信号链路与 UF20 功能查询一键加入转码工作台同构: + +```mermaid +sequenceDiagram + participant U as 用户 + participant S as TableSearchPage(映射查询) + participant M as MainWindow + participant T as TableMappingPage(表替换) + + U->>S: 双击结果行 + S->>S: 取第 1 列(UF20表名)/第 3 列(内存表名),
任一为空即无完整映射关系,tip 警告 + S->>M: requestAddMapping(uf20Name, memName) + M->>T: appendPairIfAbsent(uf20Name, memName) + Note over T: trimmed 口径判重(与添加/收集同口径);
已存在返回提示(含当前映射值,不覆盖);
新增入模型置脏后立即落盘(自动保存) + M->>S: onMappingAddResult(error) + S->>U: tip 反馈:成功"已添加到表替换并自动保存" / 失败原因 +``` + +**要点:** + +- **完整映射校验**:仅当该行 UF20 表名与内存表名均非空(即有完整映射关系)才可添加;单侧行(无映射)双击提示警告不入库; +- **判重不覆盖**:UF20 表名在表替换中已存在时跳过,tip 注明当前映射值——与表替换页手动添加口径一致,避免隐式覆盖; +- **自动保存语义**:`appendPairIfAbsent` 追加到模型并置 `m_dirty` 后立即调用 `saveToDisk()` 落盘(无弹窗,成功静默)——`onSave` 的弹窗壳与落盘核心分离,保存窗口(监控抑制)逻辑共用;自动保存失败时原因带回映射查询页 tip 展示; +- **table_mapping.json 同步**:表替换页覆写基类落盘钩子 `onSavedToDisk()`——保存成功(含双击自动保存)后,以表替换模型为源与 `table_mapping.json` 的 `tableCompareUf2_Uf3` 对象逐成员对账: + - **新增**:模型中存在、tm 中缺失的 key(且 value 非空)按原缩进/换行格式追加到对象末尾; + - **修改**:tm 中为字符串值(一对一)且与模型 value 不同的条目就地更新为模型值; + - **一对多数组跳过**:tm 中 value 为数组的条目(41 个)一律保留原文不覆盖——表替换是单值模型,覆盖会丢失其余映射;跳过数计入同步日志; + - **删除暂不同步**:表替换删除条目仅作用于 cust.json(转码口径),tm 中同名条目保留(tm 是全量参考映射,含 328 个 cust 未维护的独有条目);空 value 也不同步; + - **实现要点**:成员级文本解析(key 串/冒号/value 串或跨行数组,括号配对且跳过字符串字面量,记录每个成员原文起止),未变更成员与数组保持原文字节不变,仅替换/追加成员行后按原缩进与换行重建对象内部;写前整体重解析校验,任何结构异常放弃写入并记错误日志;同步结果记 info 日志(新增 N,更新 M,一对多数组跳过 X,空值跳过 Y); + - 映射查询页每次查询现读该文件,同步后的新增/修改即时可查; +- **页面持有**:`MainWindow` 新增 `m_tableMappingPage` 成员,页面创建 lambda 运行时解引用(构造时序上映射查询页先建、表替换页后建,两页构造完成后成员指针非空)。 + ### 5.5 代码检索模块(CodeSearchPage + CallChainAnalyzer) **职责**:功能**调用链路查询**(非内容检索)——展示某功能的向上调用方(谁调用了它)与向下被调(它调用了谁),用于转码前评估影响范围、转码后核对结果。 @@ -626,7 +683,8 @@ flowchart TD - **写闸门**:`saveDataset()` 全程持有 `m_writeGate` 写闸门;`clearCache()` 以 `tryLock` 探测在途落库任务——在途时仅重置内存状态并记录警告、跳过连接重置,避免关闭正在写入的连接(闸门在 `database()` 取连接之后获取,锁序安全无死锁,详见 §8.4); - **内存功能索引**:`refreshFunctionCache()` 将两库功能表加载至内存(`FunctionInfo{cname, eName, functionNo}`),支撑功能查询页即时检索;启动时 `MainWindow` 经 `warmupFunctionCacheAsync(db)` **异步预热**(后台线程构建内存索引,幂等:已就绪/在途跳过,详见 §8.6); - **检索 API**:`searchUF20Functions()` / `searchUFT3Functions()` 支持按中文名/英文名/功能号字段过滤,均有**单词与多词两个重载**——多词版(`QStringList` + `keywordsWithoutMatch` 出参)实现 OR 并集、按 `cname|functionNo` 去重与零命中统计,单词版内部委托多词版;两库共用匿名命名空间 `filterFunctionInfos()` 过滤实现——代码检索"按功能号检索"即经 `searchUFT3Functions(raw, "function_no", true)` 将功能号解析为函数英文名; -- **数据集 API**:`datasetExists()` / `getAllRows()` / `getRowData()` 供功能查询页读取指定功能完整数据。 +- **数据集 API**:`datasetExists()` / `getAllRows()` / `getRowData()` 供功能查询页读取指定功能完整数据; +- **表查询映射缓存**:`getUf20TableNameMap()` / `getUft3TableNameMap()` 返回"表名→中文名"全量轻量映射(SQL 层 `json_extract` 只取中文名字段,避免整表搬运大 JSON;`json_extract` 不可用时回退 `getAllRows` + 逐行解析),首次调用构建后内存缓存,随 `refreshFunctionCache()` 一并失效重建,供 UF20 & 内存表映射查询页使用(见 §5.4.3)。 ### 6.2 LogManager(单例) @@ -726,7 +784,7 @@ flowchart TD | `tableCompareUf2_Uf3` | UF20→UFT3 表名映射 | 全量/按需转码的表替换 | | `functionReplace` | 函数替换规则 | 转码的函数替换 | | `noChangeFullNameCode` | 字符串数组(功能函数全名清单) | 转码引擎对清单内功能函数名不做全名替换 | -| `convert.dir` | 对象数组 `{name, core, viaUmgr, dialect}`(全量转码清单,V2 格式,见 `docs/转码配置格式契约.md`) | 全量转码页维护、引擎读取 | +| `convert.dir` | 对象数组 `{name, core, viaUmgr, dialect}`(全量转码清单,V2 格式,见 §7.5) | 全量转码页维护、引擎读取 | | 其余键 | 引擎自身配置 | 原样透传 | - 数据映射三页各自只读写所属顶层键(键名统一引用 `Constants::CustKeys`,见 §6.4),其余内容原样保留; @@ -758,6 +816,140 @@ flowchart TD | config.ini | `[projectPath]` | UFT30/UF20/UF20账户/清算项目环境路径(`uft30` / `uf20` / `ufAcct20` / `UftdbSett`)、转码生成路径;保存时同步维护 `uft30pub = \upub\dev_codes` | | change_code.ini | `[log]` | 日志级别/开关,LogManager 启动时读取 | +### 7.5 转码配置格式契约(cust.json / cust_nonls.json) + +> 版本:1.0(2026-09-16) +> 变更发起方:转码工具 UI(Uft30ChangeCode,Qt 客户端) +> 契约接收方:转码引擎(uf2touft3) +> 状态:已评审确认,待引擎侧实现联调 + +#### 1. 变更背景 + +全量转码页新增【核心】【是否柜面接口】两列,按需转码页新增【db模式】列(支持 SQL 的 db/mdb 语法转换)。相应地,两份配置文件的 `convert.dir` 节点格式升级,引擎需按本契约完成解析与转码逻辑适配。 + +#### 2. cust.json(全量转码清单) + +##### 2.1 格式变更 + +```jsonc +// 旧格式(V1):字符串数组 +"convert": { + "dir": [ + "LS_证券参数_股票名单增加.service_design" + ], + "ext": [ ".service_design", ".function_design", ... ] +} + +// 新格式(V2):对象数组 +"convert": { + "dir": [ + { + "name": "LS_债券质押协议回购_初始交易委托.service_design", + "core": "ucbp", + "viaUmgr": true, + "dialect": "db" + } + ], + "ext": [ ".service_design", ".function_design", ... ] +} +``` + +##### 2.2 字段定义 + +| 字段 | 类型 | 必填 | 语义 | 合法取值 | +|---|---|---|---|---| +| `name` | string | 是 | 功能短名 + 设计器扩展名(后缀由 UI 按清单扩展名拼接,当前固定 `.service_design`) | 非空字符串 | +| `core` | string | 是 | 目标核心 | `"ucbp"` \| `"uses"` \| `"uarg"` \| `"usms"` | +| `viaUmgr` | bool | 是 | 是否柜面接口 | `true` \| `false` | +| `dialect` | string | 是 | SQL 语法方言,由 `core` 派生,UI 不提供独立修改入口 | `"mdb"` \| `"db"` | + +**dialect 派生规则(UI 侧保证,引擎可据此校验):** + +| core | dialect | +|---|---| +| `ucbp` | `mdb` | +| `uses` | `mdb` | +| `uarg` | `db` | +| `usms` | `db` | + +##### 2.3 引擎解析要求 + +1. **双格式兼容**:`dir` 元素为字符串时按旧格式处理(无 core/viaUmgr/dialect,行为与现有引擎一致);为对象时按新格式处理。两者可混合出现的可能性极低,但解析器不应因此崩溃。 +2. **字段兜底建议**:对象缺字段时,建议按 UI 默认值兜底并记录警告日志——`core="ucbp"`、`viaUmgr=false`、`dialect` 按 2.2 派生表由 core 推导。 +3. **非法值处理**:`core` 不在枚举内、`dialect` 与派生规则矛盾时,记录警告并按 `dialect` 字段实际值执行(以文件为准,不做强制纠正),或按团队约定明确拒绝该条目——**请引擎侧反馈选定哪种策略**。 +4. `viaUmgr=true` 时柜面接口的转码行为差异由引擎侧定义并实现。 + +#### 3. cust_nonls.json(按需转码清单) + +##### 3.1 格式变更 + +```jsonc +// 旧格式(V1):功能名 → 数值 +"convert": { + "dir": { + "*": -1, + "AS_证券日终(用户)_证券信息更新": 0 + }, + "ext": [ ".service_design", ... ] +} + +// 新格式(V2):清单条目为对象;引擎兜底键 "*" 保持数值原样 +"convert": { + "dir": { + "AS_证券参数_交易风险提示证券代码批量导入": { "depth": 0, "dialect": "db" }, + "*": -1 + }, + "ext": [ ".service_design", ... ] +} +``` + +##### 3.2 字段定义 + +| 字段 | 类型 | 必填 | 语义 | 合法取值 | +|---|---|---|---|---| +| `depth` | int | 是 | 递归深度标志,语义与旧格式数值完全一致:`0`=否(单文件转码)、`-1`=是(递归转码) | `0` \| `-1` | +| `dialect` | string | 是 | SQL 语法方言,对应 UI【db模式】列:选"是"→`db`,选"否"→`mdb` | `"db"` \| `"mdb"` | + +##### 3.3 特殊键与解析要求 + +1. **`"*"` 兜底键**:保持旧格式数值(`0`/`-1`),**不升级为对象、不携带 dialect**。引擎对非清单功能的默认行为不变,UI 侧原样透传、不展示、不修改。 +2. **双格式兼容**:`dir` 条目值为数值时按旧格式处理(dialect 默认 `mdb`);为对象时按新格式解析。 +3. **字段兜底建议**:对象缺 `depth` 按 `0`,缺 `dialect` 按 `mdb`,均记警告日志。 +4. **旧格式数组兼容**:更早期版本 `dir` 为字符串数组(无递归标志),引擎若仍有该格式在途配置,维持现有兼容逻辑即可。 + +#### 4. UI 侧写入保证(引擎可信赖的前提) + +| 保证项 | 说明 | +|---|---| +| 原子写入 | 两份配置均经 `QSaveFile` 原子提交,不会出现写一半的截断文件 | +| JSON 合法性 | cust.json 文本级替换写回前做整体 JSON 解析校验,非法即中止保存 | +| 字段完整性 | V2 对象条目四字段/两字段总是齐全,不输出缺字段条目(兜底逻辑仅为防御外部手改) | +| `ext` 节点 | 不变,引擎按现状使用 | +| 顶层键顺序 | cust.json 其余顶层键与格式不被重排(文本级区间替换) | + +#### 5. 版本与联调约定 + +1. **不兼容变更**:V2 格式对旧引擎不可解析(全量转码清单会为空)。**UI 与引擎必须同版本发布**,禁止单独升级任一侧。 +2. **升级方向兼容**:新引擎需兼容读 V1(存量用户配置),新 UI 兼容读 V1/V2。 +3. **回退方案**:若需回退到旧 UI,旧 UI 读 V2 配置会得到空清单,需手工将配置改回 V1 或删除配置重新生成。 +4. **待引擎侧反馈**: + - 2.3-3 非法值处理策略(告警放行 or 拒绝条目); + - `viaUmgr=true` 柜面接口转码的具体行为定义; + - `dialect` 驱动 SQL 语法转换(mdb/db)的实现范围与差异点清单。 + +#### 6. 格式速查(供引擎实现对照) + +``` +cust.json convert.dir : V1 string[] → V2 {name, core, viaUmgr, dialect}[] + core 枚举 : ucbp | uses | uarg | usms + dialect 派生 : ucbp/uses→mdb ; uarg/usms→db + +cust_nonls.json convert.dir : V1 {name: 0|-1, "*": -1} + → V2 {name: {depth: 0|-1, dialect: "db"|"mdb"}, "*": -1} + depth 语义 : 0=单文件转码 ; -1=递归转码(与 V1 数值一致) + "*" 键 : 保持数值,恒不升级为对象 +``` + --- ## 8. 关键设计决策与问题记录 @@ -825,7 +1017,7 @@ MainWindow 启动即校验 `bin/uf2touft3/uf2touft3.exe` 是否存在:缺失 | --- | --- | | 主窗口与导航、引擎调度 | `src/mainwindow/` | | 业务转码 | `src/pages/businessconvert/`(容器)+ `src/pages/batchconvert/`(全量)+ `src/pages/nonlsconvert/`(按需) | -| 功能查询 | `src/pages/functionsearch/`(UF20 / UFT3 双模式) | +| 功能查询 | `src/pages/functionsearch/`(UF20 / UFT3 双模式 + UF20 & 内存表映射查询) | | 代码检索 | `src/pages/codesearch/`(含 CallChainAnalyzer) | | 数据映射 | `src/pages/datamapping/`(TableMappingPage / FunctionMappingPage / NoChangeFullNamePage) | | 元数据更新 | `src/pages/metadataupdate/`(页面)+ `src/metadataupdate/`(解析器组) | diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index eb3e134..0492ae6 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -155,6 +155,8 @@ void MainWindow::initContent() markPage("UF20功能查询"); addPageNode("UFT3功能查询", createUFT3FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus); markPage("UFT3功能查询"); + addPageNode("UF20 & 内存表映射查询", createTableSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus); + markPage("UF20 & 内存表映射查询"); expandNavigationNode(m_funcConvertKey); addExpanderNode("数据映射", m_dataMappingKey, ElaIconType::TableList); @@ -260,6 +262,24 @@ QWidget* MainWindow::createUFT3FunctionSearchPage() return page; } +QWidget* MainWindow::createTableSearchPage() +{ + // UF20 & 内存表映射查询仅作检索展示, 不接入转码工作台; + // 双击记录行 → 添加映射到「表替换」页(追加后自动落盘保存) + TableSearchPage *page = new TableSearchPage; + connect(page, &TableSearchPage::requestAddMapping, this, + [this, page](const QString &uf20Name, const QString &memName) { + QString error; + if (m_tableMappingPage) { + error = m_tableMappingPage->appendPairIfAbsent(uf20Name, memName); + } else { + error = "表替换页面未初始化,无法添加"; + } + page->onMappingAddResult(error); + }); + return page; +} + QWidget* MainWindow::createSettingsPage() { return new SettingsPage(); @@ -272,7 +292,9 @@ QWidget* MainWindow::createMetadataUpdatePage() QWidget* MainWindow::createTableMappingPage() { - return new TableMappingPage(); + // 持有实例: 供映射查询页双击添加条目(lambda 运行时两页均已创建) + m_tableMappingPage = new TableMappingPage(); + return m_tableMappingPage; } QWidget* MainWindow::createFunctionMappingPage() diff --git a/src/mainwindow/mainwindow.h b/src/mainwindow/mainwindow.h index ee24213..0601db2 100644 --- a/src/mainwindow/mainwindow.h +++ b/src/mainwindow/mainwindow.h @@ -10,6 +10,7 @@ #include "src/pages/codesearch/codesearchpage.h" #include "src/pages/functionsearch/uf20functionsearchpage.h" #include "src/pages/functionsearch/uft3functionsearchpage.h" +#include "src/pages/functionsearch/tablesearchpage.h" #include "src/pages/about/aboutdialog.h" #include "src/pages/settings/settingspage.h" @@ -37,6 +38,7 @@ private: QWidget* createCodeSearchPage(); QWidget* createUF20FunctionSearchPage(); QWidget* createUFT3FunctionSearchPage(); + QWidget* createTableSearchPage(); QWidget* createSettingsPage(); QWidget* createMetadataUpdatePage(); QWidget* createTableMappingPage(); @@ -74,6 +76,7 @@ private: BatchConvertPage *m_batchConvertPage; NonLsConvertPage *m_nonLsConvertPage; CodeSearchPage *_codeSearchPage; + TableMappingPage *m_tableMappingPage = nullptr; // 表替换页实例: 供映射查询页双击添加条目 VersionChecker *m_versionChecker = nullptr; // 复用: 每次"检查更新"都 new 会累积泄漏 ConvertKind m_activeKind; diff --git a/src/pages/batchconvert/batchconvertpage.cpp b/src/pages/batchconvert/batchconvertpage.cpp index 6ca5f81..eefb6e5 100644 --- a/src/pages/batchconvert/batchconvertpage.cpp +++ b/src/pages/batchconvert/batchconvertpage.cpp @@ -20,7 +20,7 @@ namespace { -// V2 默认值(契约 docs/转码配置格式契约.md): core 默认 ucbp, 柜面接口默认否; +// V2 默认值(契约 详细设计文档.md §7.5): core 默认 ucbp, 柜面接口默认否; // dialect 不落 UI, 保存时由 core 派生(ucbp/uses→mdb, uarg/usms→db) bool validateLsFunctionNames(QWidget *parent, const QStringList &functionNames) @@ -355,6 +355,11 @@ bool BatchConvertPage::saveConfig(const QStringList &funcList) } // 5. 原子写回(二进制模式: 内容字节原样落盘, 不做任何换行转换) + // 保存窗口: 写入前开启, 窗口期内 QSaveFile 替换目标文件引发的 + // fileChanged/directoryChanged 监控事件直接忽略, 防止自身保存触发重载 + // (重载会 refreshTable 强制 scrollToTop, 表现为下拉选择后滚动条回顶); + // 失败路径(打开/提交失败)由组件 2s 兜底自动关窗 + m_configMonitor->beginSaveWindow(); QSaveFile saveFile(configFilePath()); if (!saveFile.open(QIODevice::WriteOnly)) { LogManager::instance()->logError( @@ -368,6 +373,8 @@ bool BatchConvertPage::saveConfig(const QStringList &funcList) return false; } + // 程序自身写入后更新监控 mtime 基准, 避免触发无谓的重载 + m_configMonitor->endSave(); return true; } diff --git a/src/pages/batchconvert/batchconvertpage.h b/src/pages/batchconvert/batchconvertpage.h index 2831dcc..bb31228 100644 --- a/src/pages/batchconvert/batchconvertpage.h +++ b/src/pages/batchconvert/batchconvertpage.h @@ -14,7 +14,7 @@ class QStandardItem; // 保存时对原文件做文本级区间替换(保留 cust.json 其余全部格式不重序列化)。 // core 决定目标核心(ucbp/uses/uarg/usms);dialect 由 core 派生、不提供独立 UI // (ucbp/uses→mdb,uarg/usms→db);viaUmgr 为是否柜面接口。 -// 格式契约见 docs/转码配置格式契约.md +// 格式契约见 详细设计文档.md §7.5 class BatchConvertPage : public ConvertWorkbenchBase { Q_OBJECT diff --git a/src/pages/codesearch/codesearchpage.cpp b/src/pages/codesearch/codesearchpage.cpp index 7f30d71..7878ee4 100644 --- a/src/pages/codesearch/codesearchpage.cpp +++ b/src/pages/codesearch/codesearchpage.cpp @@ -63,7 +63,7 @@ namespace { for (const QJsonValue &v : dirArray) { // V1 字符串元素 / V2 对象元素(取 name 字段)双格式兼容 - // (全量转码 convert.dir 格式升级, 见 docs/转码配置格式契约.md) + // (全量转码 convert.dir 格式升级, 见 详细设计文档.md §7.5) QString name = v.isObject() ? v.toObject().value(Constants::DirEntry::Name).toString() : v.toString(); @@ -960,12 +960,29 @@ void CodeSearchPage::onQueryByInput() return; } const QList results = cache->searchUFT3Functions(raw, Constants::FunctionSearch::FieldFunctionNo, true); - if (results.isEmpty() || results.first().eName.isEmpty()) { - QMessageBox::information(this, "提示", - QString("未找到功能号:%1 对应的函数").arg(raw)); - return; + if (!results.isEmpty() && !results.first().eName.isEmpty()) { + target = results.first().eName; + } else { + // 元数据查不到时回退代码索引按 objectId 查找: 元数据接口扫描会排除 + // cbp/ucbp 目录(moduleGenerationCheck 开启时)且同名中文名接口互相覆盖, + // 部分功能号在元数据中缺失; 代码索引全量覆盖检索目录, 无此限制。 + // 后台任务可能正在重建索引(写 objectId 哈希), 主线程读前须确认无在途任务 + if (!m_activeWatchers.isEmpty()) { + QMessageBox::information(this, "提示", "当前有任务正在执行, 请稍候再试"); + return; + } + const QList> byId = + currentAnalyzer()->lookupByObjectId(raw); + if (byId.isEmpty()) { + QMessageBox::information(this, "提示", + QString("未找到功能号:%1 对应的函数\n" + "请确认该功能号属于 UF30 工程接口,且元数据已更新、" + "代码检索路径正确并已刷新索引。").arg(raw)); + return; + } + // 同功能号多接口时取第一个候选(均在当前检索目录内, 皆可展示调用链) + target = byId.first()->chineseName; } - target = results.first().eName; } runQuery(target); diff --git a/src/pages/common/convertworkbenchbase.cpp b/src/pages/common/convertworkbenchbase.cpp index f63a1f1..e3f8760 100644 --- a/src/pages/common/convertworkbenchbase.cpp +++ b/src/pages/common/convertworkbenchbase.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include "ElaPushButton.h" #include "ElaTheme.h" @@ -196,6 +198,11 @@ QStandardItem *ConvertWorkbenchBase::createNameItem(const QString &funcName) void ConvertWorkbenchBase::refreshTable() { + // 记录重建前的滚动像素位置: 编辑/增删/外部重载后恢复原位, 避免全量重建 + // 后视图跳顶(曾表现为"修改 UF20 函数名后滚动条自动跳回顶部") + QScrollBar *vbar = m_funcTable->verticalScrollBar(); + const int savedScrollPos = vbar->value(); + // 不使用 blockSignals:QTableView 依赖 rowsRemoved/rowsInserted 信号更新内部布局缓存, // 若屏蔽会导致新增行不可见。 // 批量重建期间抑制逐行重绘:N 行的 removeRow/appendRow/setIndexWidget 每步都会触发 @@ -234,11 +241,24 @@ void ConvertWorkbenchBase::refreshTable() // 恢复视图更新并整体重绘一次 m_funcTable->setUpdatesEnabled(true); - // 视图刷新保险:强制重绘并滚动到顶部,确保新增行可见 + // 视图刷新保险:强制重绘;恢复滚动位置。 + // 关键时序: 重建后滚动范围(maximum)要等 Qt 延迟布局事件才最终确定, 直接 setValue + // 会被 clamp(表现为位置漂移到中间); scrollTo(PositionAtTop) 按视口相对坐标换算, + // 在布局中间态下会错位甚至因"目标已可见"短路不滚。因此先 doItemsLayout() 强制 + // 同步完成延迟布局(滚动范围就绪), 再按绝对像素值精确恢复 m_funcTable->viewport()->update(); m_funcTable->resizeRowsToContents(); - m_funcTable->scrollToTop(); + m_funcTable->doItemsLayout(); + vbar->setValue(savedScrollPos); m_funcTable->clearSelection(); + + // 延迟一拍兜底补设: 布局/滚动范围在此后才最终确定的场景下校正, + // 防止其他代码在重建后改动滚动条导致位置漂移 + QTimer::singleShot(0, m_funcTable, [vbar, savedScrollPos]() { + if (vbar->value() != savedScrollPos) { + vbar->setValue(savedScrollPos); + } + }); } void ConvertWorkbenchBase::refreshExistColumn() @@ -283,6 +303,16 @@ void ConvertWorkbenchBase::showEvent(QShowEvent *event) // 构造期 loadConfig 计算列宽时页面尚未布局, viewport 宽度非真实值, // 首次切入本页时宽度已就绪, 重算一次保证列宽比例正确(幂等, 开销可忽略) applyColumnLayout(); + // 行高定稿: 页面在启动期(窗口未显示、字体/控件未 polish)构建, 那时 refreshTable + // 的 resizeRowsToContents 算出的行高偏小(如 28px); 首次编辑触发的重建在可见 + // 状态下算出最终行高(如 33px), 行高突变使同一滚动数值下的可视内容整体位移, + // 表现为"首次修改后滚动条回滚"。切页显示后补算一次, 让行高在用户操作前定稿 + // (延迟一拍确保布局事件处理完毕后再测量, 测量结果与重建路径一致) + QTimer::singleShot(0, this, [this]() { + if (m_funcTable && isVisible()) { + m_funcTable->resizeRowsToContents(); + } + }); // 元数据更新后 DataCache 可能变化: 切页回来仅轻量刷新"是否存在"列, // 不做全量表格重建(数据变更已由 refreshTable/外部修改监控统一触发) refreshExistColumn(); diff --git a/src/pages/datamapping/mappingtablepage.cpp b/src/pages/datamapping/mappingtablepage.cpp index 19d4d4b..51f6fd0 100644 --- a/src/pages/datamapping/mappingtablepage.cpp +++ b/src/pages/datamapping/mappingtablepage.cpp @@ -210,6 +210,21 @@ void MappingTablePage::onModelItemChanged(QStandardItem *item) void MappingTablePage::onSave() { + QString errorMessage; + const SaveResult result = saveToDisk(&errorMessage); + if (result == SaveResult::Ok) { + QMessageBox::information(this, "提示", "保存成功!"); + } else if (result == SaveResult::ValidationFailed) { + QMessageBox::warning(this, "提示", errorMessage); + } else { + QMessageBox::critical(this, "错误", errorMessage); + } +} + +MappingTablePage::SaveResult MappingTablePage::saveToDisk(QString *errorMessage) +{ + if (errorMessage) errorMessage->clear(); + // 开启保存窗口: 窗口期内监控事件直接忽略 m_monitor->beginSaveWindow(); @@ -218,8 +233,8 @@ void MappingTablePage::onSave() QFile file(jsonPath); if (!file.open(QIODevice::ReadOnly)) { m_monitor->endSave(); // 早退必须关闭窗口,否则监控被永久抑制 - QMessageBox::critical(this, "错误", "无法读取 cust.json!"); - return; + if (errorMessage) *errorMessage = "无法读取 cust.json!"; + return SaveResult::Error; } const QString rawJson = QString::fromUtf8(file.readAll()); file.close(); @@ -228,21 +243,21 @@ void MappingTablePage::onSave() QJsonDocument doc = QJsonDocument::fromJson(rawJson.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { m_monitor->endSave(); - QMessageBox::critical(this, "错误", "cust.json 解析失败: " + error.errorString()); - return; + if (errorMessage) *errorMessage = "cust.json 解析失败: " + error.errorString(); + return SaveResult::Error; } QJsonObject custJson = doc.object(); // 提取原始 key 顺序 const QStringList keyOrder = CustJsonIo::extractTopLevelKeyOrder(rawJson); - // 子类校验并回填数据: 校验失败置 errorMessage, 弹警告不落盘 - QString errorMessage; - const QHash overrideRaw = collectMappingData(custJson, errorMessage); - if (!errorMessage.isEmpty()) { + // 子类校验并回填数据: 校验失败不落盘 + QString validateError; + const QHash overrideRaw = collectMappingData(custJson, validateError); + if (!validateError.isEmpty()) { m_monitor->endSave(); - QMessageBox::warning(this, "提示", errorMessage); - return; + if (errorMessage) *errorMessage = validateError; + return SaveResult::ValidationFailed; } // endSave 以保存后的新 mtime 为基准(writeCustJsonOrdered 同步写完才返回), @@ -254,10 +269,15 @@ void MappingTablePage::onSave() if (writeOk) { m_dirty = false; // 保存成功后与磁盘内容一致 LogManager::instance()->logInfo(saveLogText(m_model->rowCount())); - QMessageBox::information(this, "提示", "保存成功!"); - } else { - QMessageBox::critical(this, "错误", "保存到 cust.json 失败!"); + onSavedToDisk(); // 落盘成功钩子(表替换页覆写: 同步 table_mapping.json) + return SaveResult::Ok; } + if (errorMessage) *errorMessage = "保存到 cust.json 失败!"; + return SaveResult::Error; +} + +void MappingTablePage::onSavedToDisk() +{ } void MappingTablePage::onReloadRequested() diff --git a/src/pages/datamapping/mappingtablepage.h b/src/pages/datamapping/mappingtablepage.h index 06fdf56..85285eb 100644 --- a/src/pages/datamapping/mappingtablepage.h +++ b/src/pages/datamapping/mappingtablepage.h @@ -73,6 +73,18 @@ protected: // 子类构造体内调用(规避构造期虚函数绑定到基类版本) void load(); + // 保存结果分类: Ok=成功落盘; ValidationFailed=子类校验未过(用户级警告); + // Error=读文件/解析/写盘失败(程序级错误) + enum class SaveResult { Ok, ValidationFailed, Error }; + + // 落盘保存核心(无任何弹窗): onSave 与「映射查询页双击自动添加保存」共用。 + // 失败原因经 errorMessage 出参带回(ValidationFailed=校验文案, Error=IO/解析文案) + SaveResult saveToDisk(QString *errorMessage); + + // 落盘成功后的钩子(默认空实现),子类按需覆写 + // (表替换页覆写: 把新增映射同步追加到 table_mapping.json) + virtual void onSavedToDisk(); + // 虚点:从 custJson 填充模型 virtual void populateModel(const QJsonObject &custJson) = 0; // 虚点:保存时校验并回填数据到 custJson;返回 overrideRaw(键名→格式化原文); diff --git a/src/pages/datamapping/tablemappingpage.cpp b/src/pages/datamapping/tablemappingpage.cpp index 580bf66..290c9ac 100644 --- a/src/pages/datamapping/tablemappingpage.cpp +++ b/src/pages/datamapping/tablemappingpage.cpp @@ -1,5 +1,24 @@ #include "tablemappingpage.h" -#include "utils/Constants.h" +#include "src/utils/Constants.h" +#include "src/utils/logmanager.h" + +#include +#include +#include +#include +#include +#include + +namespace { + // JSON 字符串转义: 表名为普通标识符, 实际仅需处理引号与反斜杠 + QString escapeJsonString(const QString &text) + { + QString out = text; + out.replace('\\', "\\\\"); + out.replace('"', "\\\""); + return out; + } +} // namespace TableMappingPage::TableMappingPage(QWidget *parent) : TwoColumnKVMappingPage(Constants::CustKeys::TableCompare, @@ -7,3 +26,248 @@ TableMappingPage::TableMappingPage(QWidget *parent) { load(); } + +void TableMappingPage::onSavedToDisk() +{ + syncTableMappingJson(); +} + +void TableMappingPage::syncTableMappingJson() +{ + // 模型当前映射(模型序): 刚落盘的 cust.json 与模型一致, 以其为同步源 + QHash modelMap; + for (int row = 0; row < m_model->rowCount(); ++row) { + QStandardItem *keyItem = m_model->item(row, 0); + if (!keyItem || keyItem->text().trimmed().isEmpty()) { + continue; // 空键行在 collectMappingData 已拦截, 正常不会走到 + } + QStandardItem *valItem = m_model->item(row, 1); + modelMap.insert(keyItem->text().trimmed(), + valItem ? valItem->text().trimmed() : QString()); + } + + const QString jsonPath = Constants::tableMappingJson(); + QFile file(jsonPath); + if (!file.open(QIODevice::ReadOnly)) { + LogManager::instance()->logError("表替换同步 - 无法读取 table_mapping.json: " + jsonPath); + return; + } + const QString raw = QString::fromUtf8(file.readAll()); + file.close(); + + QJsonParseError parseErr; + const QJsonDocument doc = QJsonDocument::fromJson(raw.toUtf8(), &parseErr); + if (parseErr.error != QJsonParseError::NoError) { + LogManager::instance()->logError( + QString("表替换同步 - table_mapping.json 解析失败: %1 (偏移 %2)") + .arg(parseErr.errorString()).arg(parseErr.offset)); + return; + } + const QJsonObject tmObj = doc.object()[Constants::CustKeys::TableCompare].toObject(); + + // 文本定位 tableCompareUf2_Uf3 对象的开/闭括号, 成员级解析后按原格式重建, + // 保持文件其余部分(条目顺序/一对多数组/空行风格)原样不动 + const QString tmKey = "\"" + Constants::CustKeys::TableCompare + "\""; + const qsizetype keyPos = raw.indexOf(tmKey); + const qsizetype openPos = keyPos < 0 ? -1 : raw.indexOf('{', keyPos + tmKey.size()); + if (openPos < 0) { + LogManager::instance()->logError( + "表替换同步 - table_mapping.json 中未找到 tableCompareUf2_Uf3 对象"); + return; + } + + // 成员描述: key + 原文起止(从 key 引号到 value 结束, 含跨行数组原文) + struct Member { + QString key; + qsizetype start; + qsizetype end; + }; + QList members; + + // 花括号/方括号配对跳过字符串字面量, 从开括号后逐成员解析 + qsizetype i = openPos + 1; + while (i < raw.size()) { + while (i < raw.size() && raw.at(i).isSpace()) { + ++i; + } + if (i >= raw.size() || raw.at(i) == '}') { + break; + } + if (raw.at(i) != '"') { + LogManager::instance()->logError( + QString("表替换同步 - tableCompareUf2_Uf3 成员解析异常(偏移 %1), 已放弃同步").arg(i)); + return; + } + const qsizetype memberStart = i; + // 解析 key 字符串(处理 \" 与 \\ 转义, 与 QJsonDocument 解析口径一致) + ++i; + QString key; + while (i < raw.size()) { + const QChar ch = raw.at(i++); + if (ch == '\\' && i < raw.size()) { + const QChar esc = raw.at(i++); + if (esc == '"' || esc == '\\') { + key += esc; + } else { + key += '\\'; + key += esc; + } + } else if (ch == '"') { + break; + } else { + key += ch; + } + } + while (i < raw.size() && raw.at(i).isSpace()) { + ++i; + } + if (i >= raw.size() || raw.at(i) != ':') { + LogManager::instance()->logError( + QString("表替换同步 - 成员 %1 缺少冒号, 已放弃同步").arg(key)); + return; + } + ++i; + while (i < raw.size() && raw.at(i).isSpace()) { + ++i; + } + // 解析 value: 字符串 / 数组 / 对象, 括号配对方括号与花括号都计数 + int bracketDepth = 0; + bool valString = false; + const qsizetype valueEnd = [&]() { + while (i < raw.size()) { + const QChar ch = raw.at(i); + if (valString) { + if (ch == '\\') { + i += 2; + continue; + } + if (ch == '"') { + valString = false; + } + ++i; + continue; + } + if (ch == '"') { + valString = true; + } else if (ch == '[' || ch == '{') { + ++bracketDepth; + } else if (ch == ']' || ch == '}') { + --bracketDepth; + if (bracketDepth < 0) { + return i; // 到达本对象闭括号(value 已结束于上一字符) + } + } else if (ch == ',' && bracketDepth == 0) { + return i; + } + ++i; + } + return i; + }(); + // memberEnd 回退 value 尾部空白, 确保原文片段以 value 末字符结尾 + qsizetype trimmedEnd = valueEnd; + while (trimmedEnd > memberStart && raw.at(trimmedEnd - 1).isSpace()) { + --trimmedEnd; + } + members.append({key, memberStart, trimmedEnd}); + i = valueEnd; + if (i < raw.size() && raw.at(i) == ',') { + ++i; + } + } + + // 缩进/换行探测: 以首个既有成员所在行的行首空白为准 + const QString newline = raw.contains("\r\n") ? "\r\n" : "\n"; + QString entryIndent(" "); // tm 当前缩进风格兜底 + QString closeIndent(" "); + if (!members.isEmpty()) { + const qsizetype lineStart = raw.lastIndexOf('\n', members.first().start) + 1; + entryIndent = raw.mid(lineStart, members.first().start - lineStart); + } + { + const qsizetype closePos = raw.indexOf('}', i); + if (closePos >= 0) { + const qsizetype lineStart = raw.lastIndexOf('\n', closePos) + 1; + closeIndent = raw.mid(lineStart, closePos - lineStart); + } + } + + // 逐成员对账: 同 key 时字符串 value 变了才更新; 数组(一对多)一律保留原文; + // 模型中多余的 key 追加到末尾。删除暂不同步(成员不因模型缺失而移除) + QStringList blocks; + QSet processedKeys; + int updated = 0; + int arraySkipped = 0; + int emptyValueSkipped = 0; + for (const Member &member : members) { + processedKeys.insert(member.key); + const auto modelIt = modelMap.constFind(member.key); + if (modelIt == modelMap.constEnd()) { + blocks.append(raw.mid(member.start, member.end - member.start)); // cust 已删: tm 保留 + continue; + } + const QJsonValue tmVal = tmObj.value(member.key); + if (tmVal.isArray()) { + blocks.append(raw.mid(member.start, member.end - member.start)); // 一对多数组不覆盖 + ++arraySkipped; + continue; + } + const QString newVal = modelIt.value(); + if (newVal.isEmpty()) { + blocks.append(raw.mid(member.start, member.end - member.start)); // 空值不同步 + ++emptyValueSkipped; + continue; + } + if (tmVal.isString() && tmVal.toString() != newVal) { + blocks.append('"' + escapeJsonString(member.key) + "\": \"" + + escapeJsonString(newVal) + '"'); + ++updated; + } else { + blocks.append(raw.mid(member.start, member.end - member.start)); + } + } + int appended = 0; + for (auto it = modelMap.constBegin(); it != modelMap.constEnd(); ++it) { + if (processedKeys.contains(it.key()) || it.value().isEmpty()) { + continue; + } + blocks.append('"' + escapeJsonString(it.key()) + "\": \"" + + escapeJsonString(it.value()) + '"'); + ++appended; + } + if (appended == 0 && updated == 0) { + return; // 无新增无更新(数组跳过/空值跳过不产生写入) + } + + // 重建对象内部: 每成员统一行首缩进, 成员间逗号分隔; 成员原文若跨行(数组), + // 其内部各行已有更大缩进, 保持原样 + QStringList indented; + indented.reserve(blocks.size()); + for (const QString &block : blocks) { + indented.append(entryIndent + block); + } + const QString interior = newline + indented.join(QString(",") + newline) + newline + closeIndent; + const QString updatedRaw = raw.left(openPos + 1) + interior + raw.mid(raw.indexOf('}', i)); + + // 写前校验: 拼接结果必须仍是合法 JSON, 防止文本插入破坏文件 + QJsonParseError verifyErr; + QJsonDocument::fromJson(updatedRaw.toUtf8(), &verifyErr); + if (verifyErr.error != QJsonParseError::NoError) { + LogManager::instance()->logError( + QString("表替换同步 - 拼接结果校验失败, 已放弃写入: %1").arg(verifyErr.errorString())); + return; + } + + QFile outFile(jsonPath); + if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + LogManager::instance()->logError("表替换同步 - 无法写入 table_mapping.json: " + jsonPath); + return; + } + outFile.write(updatedRaw.toUtf8()); + outFile.close(); + + LogManager::instance()->logInfo( + QString("表替换保存: table_mapping.json 同步完成(新增 %1, 更新 %2%3%4)") + .arg(appended).arg(updated) + .arg(arraySkipped > 0 ? QString(", 一对多数组跳过 %1").arg(arraySkipped) : QString()) + .arg(emptyValueSkipped > 0 ? QString(", 空值跳过 %1").arg(emptyValueSkipped) : QString())); +} diff --git a/src/pages/datamapping/tablemappingpage.h b/src/pages/datamapping/tablemappingpage.h index 42279ba..530bdc6 100644 --- a/src/pages/datamapping/tablemappingpage.h +++ b/src/pages/datamapping/tablemappingpage.h @@ -5,13 +5,22 @@ // 维护 cust.json 顶层键 tableCompareUf2_Uf3(UF20→UFT3 表替换映射): // 两列映射表, 支持 新增(弹窗)/双击修改/删除/搜索过滤, 保存按顶层键顺序保序写回; -// 填充/校验/新增弹窗样板由 TwoColumnKVMappingPage 中间基类收编 +// 填充/校验/新增弹窗样板由 TwoColumnKVMappingPage 中间基类收编。 +// 保存成功后把模型中 table_mapping.json 缺失的新增 key 追加到该文件 +// (只增不改: 已有条目含一对多数组一律以 table_mapping.json 为准) class TableMappingPage : public TwoColumnKVMappingPage { Q_OBJECT public: explicit TableMappingPage(QWidget *parent = nullptr); + +protected: + void onSavedToDisk() override; + +private: + // 把模型中 table_mapping.json 缺失的 key 追加到该文件(文本定位插入, 保序保格式) + void syncTableMappingJson(); }; #endif // TABLEMAPPINGPAGE_H diff --git a/src/pages/datamapping/twocolumnkvmappingpage.cpp b/src/pages/datamapping/twocolumnkvmappingpage.cpp index e9059d2..f9a028b 100644 --- a/src/pages/datamapping/twocolumnkvmappingpage.cpp +++ b/src/pages/datamapping/twocolumnkvmappingpage.cpp @@ -73,6 +73,35 @@ QHash TwoColumnKVMappingPage::collectMappingData(QJsonObject & CustJsonIo::formatOrderedJsonObject(orderedData)}}; } +QString TwoColumnKVMappingPage::appendPairIfAbsent(const QString &key, const QString &value) +{ + // 判重口径与 onAddRow/collectMappingData 一致: trimmed 后比较 + const QString k = key.trimmed(); + if (k.isEmpty()) { + return QString("UF20 %1为空,无法添加").arg(m_entityName); + } + for (int row = 0; row < m_model->rowCount(); ++row) { + if (m_model->item(row, 0) && m_model->item(row, 0)->text().trimmed() == k) { + return QString("UF20 %1 \"%2\" 在表替换中已存在(当前映射: %3),未重复添加") + .arg(m_entityName).arg(k) + .arg(m_model->item(row, 1) ? m_model->item(row, 1)->text() : QString()); + } + } + QList rowItems; + rowItems.append(new QStandardItem(k)); + rowItems.append(new QStandardItem(value.trimmed())); + m_model->appendRow(rowItems); + // 显式置脏纳入外部文件变更保护, 随后立即落盘保存(自动保存语义, + // 无弹窗: 成功静默, 失败把原因带回给调用页 tip 展示) + m_dirty = true; + QString saveError; + const MappingTablePage::SaveResult result = saveToDisk(&saveError); + if (result != MappingTablePage::SaveResult::Ok) { + return QString("已添加但自动保存失败:%1").arg(saveError); + } + return QString(); +} + void TwoColumnKVMappingPage::onAddRow() { QDialog dialog(this); diff --git a/src/pages/datamapping/twocolumnkvmappingpage.h b/src/pages/datamapping/twocolumnkvmappingpage.h index 68afb43..331c562 100644 --- a/src/pages/datamapping/twocolumnkvmappingpage.h +++ b/src/pages/datamapping/twocolumnkvmappingpage.h @@ -23,6 +23,12 @@ public: const QString &entityName, QWidget *parent = nullptr); + // 追加一对映射(UF20 & 内存表映射查询页双击添加的入口)。 + // key 按 trimmed 口径判重: 已存在返回提示文案(含当前映射值,不覆盖)。 + // 追加成功后自动落盘保存(无弹窗),失败时返回"已添加但自动保存失败:原因"文案, + // 成功返回空串 + QString appendPairIfAbsent(const QString &key, const QString &value); + protected: void populateModel(const QJsonObject &custJson) override; QHash collectMappingData(QJsonObject &custJson, diff --git a/src/pages/functionsearch/tablesearchpage.cpp b/src/pages/functionsearch/tablesearchpage.cpp new file mode 100644 index 0000000..80308b7 --- /dev/null +++ b/src/pages/functionsearch/tablesearchpage.cpp @@ -0,0 +1,371 @@ +#include "tablesearchpage.h" +#include "src/utils/Constants.h" +#include "src/utils/datacache.h" +#include "src/utils/logmanager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ElaTheme.h" + +namespace { + // 拆分多关键词: 中英文逗号/分号 + 空白统一视为分隔符(与功能查询页口径一致) + // 全角字符用字符串字面量替换, 多字节字符不能写成字符常量(会成为 multichar) + const QRegularExpression kWhitespaceRegex(QStringLiteral("\\s+")); + + QStringList splitKeywords(const QString& text) + { + QString normalized = text; + normalized.replace(QStringLiteral(","), QStringLiteral(" ")) + .replace(QStringLiteral(";"), QStringLiteral(" ")) + .replace(',', ' ') + .replace(';', ' '); + return normalized.split(kWhitespaceRegex, Qt::SkipEmptyParts); + } +} + +TableSearchPage::TableSearchPage(QWidget *parent) + : QWidget(parent) + , m_tipLabel(nullptr) + , m_exactMatch(false) +{ + initUI(); +} + +void TableSearchPage::initUI() +{ + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(30, 30, 30, 30); + layout->setSpacing(20); + + QHBoxLayout *searchLayout = new QHBoxLayout; + searchLayout->setSpacing(10); + + m_searchEdit = new ElaSearchEdit; + m_searchEdit->setPlaceholderText("搜索 UF20 表名或内存表名..."); + m_searchEdit->setSearchEditSize(330, 35); + connect(m_searchEdit, &ElaSearchEdit::returnPressed, this, &TableSearchPage::onSearch); + connect(m_searchEdit, &ElaSearchEdit::searchClicked, this, &TableSearchPage::onSearch); + connect(m_searchEdit, &ElaSearchEdit::exactMatchChanged, this, &TableSearchPage::onExactMatchToggled); + searchLayout->addWidget(m_searchEdit); + searchLayout->addStretch(); + layout->addLayout(searchLayout); + + m_tableModel = new QStandardItemModel(this); + m_tableModel->setColumnCount(4); + m_tableModel->setHorizontalHeaderLabels({"UF20表名", "UF20表中文名", "内存表名", "内存表中文名"}); + + m_resultTable = new ElaTableView; + m_resultTable->setModel(m_tableModel); + m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + m_resultTable->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter); // 表头居中 + m_resultTable->setMinimumHeight(400); + m_resultTable->setSelectionBehavior(QAbstractItemView::SelectRows); + m_resultTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + layout->addWidget(m_resultTable); + + // 双击记录行 → 添加映射到「数据映射 → 表替换」页(经 MainWindow 接线) + connect(m_resultTable, &QTableView::doubleClicked, this, &TableSearchPage::onDoubleClicked); + + QHBoxLayout *tipLayout = new QHBoxLayout; + m_tipLabel = new QLabel("💡 输入 UF20 表名或内存表名进行查询,支持精确/模糊匹配;双击记录行可添加到「数据映射 → 表替换」"); + // 不开折行: wordWrap(true) 的 QLabel 在布局中会自选偏窄宽度把提示拆成多行, + // 与功能查询页保持一致, 提示单行显示 + m_tipLabel->setWordWrap(false); + tipLayout->addWidget(m_tipLabel); + tipLayout->addStretch(); + layout->addLayout(tipLayout); + + // tip 标签为主题感知: 跟随 Ela 主题切换, 并按当前主题初始化 + connect(ElaTheme::getInstance(), &ElaTheme::themeModeChanged, + this, &TableSearchPage::updateTipStyle); + updateTipStyle(); +} + +void TableSearchPage::updateTipStyle() +{ + if (!m_tipLabel) { + return; + } + const ElaThemeType::ThemeMode mode = ElaTheme::getInstance()->getThemeMode(); + const QString textColor = ElaThemeColor(mode, BasicText).name(); + m_tipLabel->setStyleSheet(QString("color: %1; font-size: 13px;").arg(textColor)); +} + +void TableSearchPage::onExactMatchToggled(bool checked) +{ + m_exactMatch = checked; +} + +void TableSearchPage::onDoubleClicked(const QModelIndex& index) +{ + // 双击行取第 1 列(UF20表名)与第 3 列(内存表名): 任一为空即无完整映射关系, 不能添加 + const QString uf20Name = m_tableModel->item(index.row(), 0) + ? m_tableModel->item(index.row(), 0)->text() : QString(); + const QString memName = m_tableModel->item(index.row(), 2) + ? m_tableModel->item(index.row(), 2)->text() : QString(); + if (uf20Name.isEmpty() || memName.isEmpty()) { + if (m_tipLabel) { + m_tipLabel->setText("⚠️ 该行无完整映射关系(UF20 表名或内存表名为空),无法添加到表替换"); + } + return; + } + emit requestAddMapping(uf20Name, memName); +} + +void TableSearchPage::onMappingAddResult(const QString& error) +{ + // 双击添加的回调结果: error 为空即添加并自动保存成功, 非空为原因(已存在/保存失败等) + if (!m_tipLabel) { + return; + } + if (error.isEmpty()) { + m_tipLabel->setText("✅ 已添加到「数据映射 → 表替换」并自动保存"); + } else { + m_tipLabel->setText("⚠️ " + error); + } +} + +QList> TableSearchPage::loadTableMapping(bool* readOk, + QString* errorOut) const +{ + if (readOk) *readOk = false; + if (errorOut) errorOut->clear(); + QList> pairs; + const QString jsonPath = Constants::tableMappingJson(); + QFile file(jsonPath); + if (!file.open(QIODevice::ReadOnly)) { + const QString msg = "无法读取映射文件 table_mapping.json(" + jsonPath + ")"; + LogManager::instance()->logError("表查询 - " + msg); + if (errorOut) *errorOut = msg; + return pairs; + } + const QByteArray raw = file.readAll(); + file.close(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error != QJsonParseError::NoError) { + // 字符偏移转行号(从 1 起), 便于手工维护文件时定位(常见手误: 末项尾随逗号) + int line = 1; + for (qint64 pos = 0; pos < err.offset && pos < raw.size(); ++pos) { + if (raw.at(pos) == '\n') { + ++line; + } + } + const QString msg = QString("映射文件 table_mapping.json 解析失败(第 %1 行附近):%2。" + "映射关系暂不可用,请检查该文件(常见原因:最后一项后多了逗号、引号缺失)") + .arg(line).arg(err.errorString()); + LogManager::instance()->logError("表查询 - " + msg); + if (errorOut) *errorOut = msg; + return pairs; + } + // 顶层键 tableCompareUf2_Uf3: key=UF20表名, value=内存表名; + // value 可为字符串(一对一)或数组(一对多, 拆为多条映射对, 展示时一个 UF20 表对应多行) + const QJsonObject mapping = doc.object()[Constants::CustKeys::TableCompare].toObject(); + for (auto it = mapping.constBegin(); it != mapping.constEnd(); ++it) { + if (it.value().isArray()) { + const QJsonArray arr = it.value().toArray(); + for (const QJsonValue& v : arr) { + pairs.append({it.key(), v.toString()}); + } + } else { + pairs.append({it.key(), it.value().toString()}); + } + } + if (readOk) *readOk = true; + return pairs; +} + +void TableSearchPage::onSearch() +{ + // 多关键词查询: 支持逗号/空格等分隔多个表名, 任一关键词命中即展示(OR 并集) + const QStringList keywords = splitKeywords(m_searchEdit->text()); + + m_tableModel->removeRows(0, m_tableModel->rowCount()); + + if (keywords.isEmpty()) { + // 空关键字: 页内 tip 反馈, 不弹模态框打断操作 + if (m_tipLabel) { + m_tipLabel->setText("请输入 UF20 表名或内存表名"); + } + return; + } + + // 数据源三件套: 双库"表名→中文名"映射 + table_mapping.json 表映射(key=UF20表名, value=内存表名)。 + // 双库映射由 DataCache 内存缓存, 重复查询零开销 + const QMap uf20Names = DataCache::instance()->getUf20TableNameMap(); + const QMap memNames = DataCache::instance()->getUft3TableNameMap(); + if (uf20Names.isEmpty() && memNames.isEmpty()) { + if (m_tipLabel) { + m_tipLabel->setText("⚠️ 缓存库中暂无表数据,请先在「元数据更新」页面执行更新"); + } + return; + } + + bool mappingReadOk = false; + QString mappingError; + const QList> mapping = loadTableMapping(&mappingReadOk, &mappingError); + // 映射文件是本页核心数据源: 读取/解析失败时醒目报错并停止检索, + // 不再降级展示单侧结果(否则表现为"有表名却查不到映射", 极具误导性) + if (!mappingReadOk) { + if (m_tipLabel) { + m_tipLabel->setText("⚠️ " + mappingError); + } + return; + } + + // 单行结果: 四列与页面列定义一致, 任一表名侧为空即"无映射关系"行 + struct TableRow { + QString uf20Name, uf20Cname, memName, memCname; + }; + QList rows; + // 已覆盖表名集合: 映射整体行里的表不再补单侧行, 防止同一表重复出现 + QSet coveredUf20; + QSet coveredMem; + // 已输出的映射对(以换行分隔拼接): 多关键词可能重复命中同一映射对, 去重 + QSet seenPairs; + + // 映射命中(key 或 value)→ 完整行: 按 key/value 分别取双库中文名 + auto appendFullRow = [&](const QString& key, const QString& value) { + const QString pairKey = key + QLatin1Char('\n') + value; + if (seenPairs.contains(pairKey)) { + return; + } + seenPairs.insert(pairKey); + rows.append({key, uf20Names.value(key), value, memNames.value(value)}); + coveredUf20.insert(key); + coveredMem.insert(value); + }; + + QStringList noMatchKeywords; + for (const QString& keyword : keywords) { + bool hit = false; + if (m_exactMatch) { + // 精确匹配: 先查映射(key 或 value 全等 → 整行), 再查双库(单侧行); + // 映射已覆盖的表不再补单侧行, 否则同一表会多出一条映射列置空的重复记录 + for (const auto& pair : mapping) { + if (pair.first == keyword || pair.second == keyword) { + appendFullRow(pair.first, pair.second); + hit = true; + } + } + if (!coveredUf20.contains(keyword) && uf20Names.contains(keyword)) { + rows.append({keyword, uf20Names.value(keyword), QString(), QString()}); + coveredUf20.insert(keyword); + hit = true; + } + if (!coveredMem.contains(keyword) && memNames.contains(keyword)) { + rows.append({QString(), QString(), keyword, memNames.value(keyword)}); + coveredMem.insert(keyword); + hit = true; + } + } else { + // 模糊匹配: 包含即命中(不区分大小写); 映射命中→整行, 库命中→单侧行 + for (const auto& pair : mapping) { + if (pair.first.contains(keyword, Qt::CaseInsensitive) || + pair.second.contains(keyword, Qt::CaseInsensitive)) { + appendFullRow(pair.first, pair.second); + hit = true; + } + } + for (auto it = uf20Names.constBegin(); it != uf20Names.constEnd(); ++it) { + if (!coveredUf20.contains(it.key()) && + it.key().contains(keyword, Qt::CaseInsensitive)) { + rows.append({it.key(), it.value(), QString(), QString()}); + coveredUf20.insert(it.key()); + hit = true; + } + } + for (auto it = memNames.constBegin(); it != memNames.constEnd(); ++it) { + if (!coveredMem.contains(it.key()) && + it.key().contains(keyword, Qt::CaseInsensitive)) { + rows.append({QString(), QString(), it.key(), it.value()}); + coveredMem.insert(it.key()); + hit = true; + } + } + } + if (!hit) { + noMatchKeywords.append(keyword); + } + } + + if (rows.isEmpty()) { + if (m_tipLabel) { + m_tipLabel->setText("⚠️ 未找到匹配的表,请确认表名是否正确(UF20 表与内存表为两套命名)"); + } + return; + } + + // 无映射行数: UF20 表名或内存表名任一侧为空即视为无映射关系; + // 中文名缺失: 表名侧有值但缓存库查不到该表(元数据更新未覆盖其来源工程), + // 中文名无从取得, 只能留空并在 tip 中提示数量 + int unmappedCount = 0; + int cnameMissingCount = 0; + for (const TableRow& row : rows) { + if (row.uf20Name.isEmpty() || row.memName.isEmpty()) { + ++unmappedCount; + } + if ((!row.uf20Name.isEmpty() && row.uf20Cname.isEmpty()) || + (!row.memName.isEmpty() && row.memCname.isEmpty())) { + ++cnameMissingCount; + } + } + + // 查询成功: tip 显示统计(一对多映射会产生多条记录); 映射读取失败时附加降级说明 + if (m_tipLabel) { + QString tip = QString("已查询 %1 条记录").arg(rows.size()); + if (unmappedCount > 0) { + tip += QString("(其中 %1 个无映射关系").arg(unmappedCount); + if (cnameMissingCount > 0) { + tip += QString(",%1 个表中文名缺失(未在缓存库收录)").arg(cnameMissingCount); + } + tip += ")"; + } else if (cnameMissingCount > 0) { + tip += QString("(%1 个表中文名缺失,未在缓存库收录)").arg(cnameMissingCount); + } + if (!noMatchKeywords.isEmpty()) { + tip += QString(",%1 个关键词无匹配").arg(noMatchKeywords.size()); + } + m_tipLabel->setText(tip); + } + + // 结果按 UF20 表名排序(不区分大小写); 仅内存表命中的行(UF20 名为空)排末尾; + // 同一 UF20 表的一对多记录按内存表名排序, 保证相邻展示 + std::sort(rows.begin(), rows.end(), [](const TableRow& a, const TableRow& b) { + if (a.uf20Name.isEmpty() != b.uf20Name.isEmpty()) { + return b.uf20Name.isEmpty(); // a 的 UF20 名非空在前 + } + const QString an = a.uf20Name.isEmpty() ? a.memName : a.uf20Name; + const QString bn = b.uf20Name.isEmpty() ? b.memName : b.uf20Name; + const int primary = an.compare(bn, Qt::CaseInsensitive); + if (primary != 0) { + return primary < 0; + } + return a.memName.compare(b.memName, Qt::CaseInsensitive) < 0; + }); + + // 批量填充期间抑制逐行重绘(与功能查询页一致): 关闭更新后仅在填充完成时整体呈现 + m_resultTable->setUpdatesEnabled(false); + for (const TableRow& row : rows) { + const QString cells[4] = {row.uf20Name, row.uf20Cname, row.memName, row.memCname}; + QList rowItems; + for (const QString& cell : cells) { + QStandardItem *item = new QStandardItem(cell); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setTextAlignment(Qt::AlignCenter); // 列内容居中 + rowItems.append(item); + } + m_tableModel->appendRow(rowItems); + } + m_resultTable->setUpdatesEnabled(true); +} diff --git a/src/pages/functionsearch/tablesearchpage.h b/src/pages/functionsearch/tablesearchpage.h new file mode 100644 index 0000000..2cff7ad --- /dev/null +++ b/src/pages/functionsearch/tablesearchpage.h @@ -0,0 +1,57 @@ +#ifndef TABLESEARCHPAGE_H +#define TABLESEARCHPAGE_H + +#include +#include +#include "components/elasearchedit.h" +#include "ElaTableView.h" + +class QLabel; + +// UF20 & 内存表映射查询页(原"表查询"): 检索 UF20(2.0) 表与内存表(UFT3)之间的映射关系。 +// 结果四列: UF20表名 / UF20表中文名(uf20_cache.db 按 key 取) / +// 内存表名(table_mapping.json 映射的 value) / 内存表中文名(uft3_cache.db 按 value 取)。 +// 输入 UF20 表名或内存表名均可检索, 支持精确/模糊(ElaSearchEdit 内置开关)两种模式; +// 无映射命中的表仅展示数据库侧字段, 映射相关列置空。 +// 双击有完整映射的记录行可添加到「数据映射 → 表替换」页(经 MainWindow 接线, 追加后自动落盘)。 +// 不继承 FunctionSearchPageBase: 结果结构为 4 列表信息, 与 FunctionInfo(3 列函数信息) +// 结构不同, 数据源与交互(无批量加入)亦不同, 强行复用基类不值 +class TableSearchPage : public QWidget +{ + Q_OBJECT + +public: + explicit TableSearchPage(QWidget *parent = nullptr); + // 双击添加的回调结果(经 MainWindow 接线后回传): error 为空即添加成功, 非空为原因 + void onMappingAddResult(const QString& error); + +signals: + // 双击有完整映射的记录行 → 请求把 (UF20表名, 内存表名) 添加到表替换页 + void requestAddMapping(const QString& uf20Name, const QString& memName); + +private slots: + void onSearch(); + void onExactMatchToggled(bool checked); + // 双击记录行: 有完整映射发出 requestAddMapping, 单侧行仅 tip 提示 + void onDoubleClicked(const QModelIndex& index); + // tip 标签主题感知(Ela 原生控件颜色不随主题切换, 需手动跟随) + void updateTipStyle(); + +private: + void initUI(); + // 读取表映射文件(Constants::tableMappingJson(), 即 bin/uf2touft3/config/table_mapping.json, + // 顶层键 tableCompareUf2_Uf3: key=UF20表名, value=内存表名; value 可为字符串(一对一) + // 或字符串数组(一对多, 拆为多条映射对))。 + // readOk 出参: 文件读取/解析成功为 true(映射本身可为空), 失败为 false; + // errorOut 出参: 失败时带回可展示的错误原因(含行号, 常见手误如末项尾随逗号) + QList> loadTableMapping(bool* readOk, + QString* errorOut = nullptr) const; + + ElaSearchEdit *m_searchEdit; + ElaTableView *m_resultTable; + QStandardItemModel *m_tableModel; + QLabel *m_tipLabel; + bool m_exactMatch; +}; + +#endif diff --git a/src/pages/nonlsconvert/nonlsconvertpage.h b/src/pages/nonlsconvert/nonlsconvertpage.h index 0fd1623..7214b8b 100644 --- a/src/pages/nonlsconvert/nonlsconvertpage.h +++ b/src/pages/nonlsconvert/nonlsconvertpage.h @@ -16,7 +16,7 @@ class QStandardItem; // 调用契约:仅携带 -m 2(按需转码模式);函数清单与递归开关由引擎直接读取 cust_nonls.json, // 不再需要 -t/-o 等命令行参数 // 历史格式兼容:旧版条目为数值(无 dialect)按"否/mdb"兜底;更早期字符串数组(无递归 -// 标志)统一按"否/mdb"兜底。格式契约见 docs/转码配置格式契约.md +// 标志)统一按"否/mdb"兜底。格式契约见 详细设计文档.md §7.5 class NonLsConvertPage : public ConvertWorkbenchBase { Q_OBJECT diff --git a/src/utils/Constants.h b/src/utils/Constants.h index 3755935..11c1a0d 100644 --- a/src/utils/Constants.h +++ b/src/utils/Constants.h @@ -66,6 +66,7 @@ namespace ToolFiles { inline const QString Cust2Json = "config/cust2.json"; inline const QString CustNonLsJson = "config/cust_nonls.json"; // 按需转码 UI 清单持久化 inline const QString NonLsRunList = "config/run_nonls_list.txt"; // 大批量时生成的 @列表文件 + inline const QString TableMappingJson = "config/table_mapping.json"; // 表查询: UF20表↔内存表映射文件 } // --------------------------------------------------------------------------- @@ -180,7 +181,7 @@ namespace DesignExts { // --------------------------------------------------------------------------- // convert.dir 条目字段与取值(V2 对象格式, 全量/按需转码页与代码检索清单读取共用, -// 拼错会静默丢字段/错方言; 契约见 docs/转码配置格式契约.md) +// 拼错会静默丢字段/错方言; 契约见 详细设计文档.md §7.5) // --------------------------------------------------------------------------- namespace DirEntry { inline const QString Name = "name"; // 功能短名+设计器扩展名(全量转码) @@ -235,6 +236,9 @@ namespace Timings { inline const int UpdateCheckDelayMs = 3000; // 启动后延迟检查版本更新 inline const int JsonDebounceMs = 300; // JSON 配置外部修改防抖 inline const int JsonSaveWindowResetMs = 2000; // 保存窗口异常路径的自动复位兜底 + inline const int JsonQuietWindowMs = 1500; // 保存后静默窗: QSaveFile 替换文件引发的 + // 目录事件风暴/杀软扫描 touch 造成的 mtime + // 漂移视为保存余波, 不判为外部修改重载 // 转换引擎整体运行超时: 超时自动走 stop() 升级链终止疑似卡死的引擎, // 防止任务无限挂起(全量大工程正常耗时可观的场景下取宽裕值) inline const int CacheWarmupWaitMs = 5000; // 功能缓存后台预热的主线程有界等待上限(超限转同步重建) @@ -267,6 +271,7 @@ inline QString toolExe() { return toolDir() + "/" + ToolFiles::Exe; } inline QString configIni() { return toolDir() + "/" + ToolFiles::ConfigIni; } inline QString custJson() { return toolDir() + "/" + ToolFiles::CustJson; } inline QString custNonLsJson() { return toolDir() + "/" + ToolFiles::CustNonLsJson; } +inline QString tableMappingJson() { return toolDir() + "/" + ToolFiles::TableMappingJson; } inline QString cacheDir() { return toolDir() + "/" + Paths::CacheDir; } // bin/uf2touft3/cache inline QString uf20CacheDb() { return cacheDir() + "/" + Cache::Uf20DbFile; } inline QString uft3CacheDb() { return cacheDir() + "/" + Cache::Uft3DbFile; } diff --git a/src/utils/callchainanalyzer.cpp b/src/utils/callchainanalyzer.cpp index 2e54eab..8dd02a1 100644 --- a/src/utils/callchainanalyzer.cpp +++ b/src/utils/callchainanalyzer.cpp @@ -102,6 +102,7 @@ bool CallChainAnalyzer::buildIndex(QString *errorMessage) { m_chineseNameIndex.clear(); m_canonicalNameIndex.clear(); + m_objectIdIndex.clear(); m_allInfos.clear(); m_indexed = false; @@ -159,6 +160,10 @@ bool CallChainAnalyzer::buildIndex(QString *errorMessage) if (!info->canonicalName.isEmpty()) { insertPrefer(m_canonicalNameIndex, info->canonicalName, info); } + // 按功能号索引(同号多文件时全量保留, 查询方按序取用) + if (!info->objectId.isEmpty()) { + m_objectIdIndex[info->objectId].append(info); + } // 登记全量集合(saveToCache 导出底座; 同名落选变体也保留, // 保证重启后 loadFromCache 重建的索引与内存一致) m_allInfos.append(info); @@ -350,6 +355,7 @@ bool CallChainAnalyzer::loadFromCache(const QString &path, QString *message, boo { m_chineseNameIndex.clear(); m_canonicalNameIndex.clear(); + m_objectIdIndex.clear(); m_allInfos.clear(); m_indexed = false; @@ -415,6 +421,10 @@ bool CallChainAnalyzer::loadFromCache(const QString &path, QString *message, boo if (!info->canonicalName.isEmpty()) { insertPrefer(m_canonicalNameIndex, info->canonicalName, info); } + // 按功能号索引, 与 buildIndex 构建口径一致 + if (!info->objectId.isEmpty()) { + m_objectIdIndex[info->objectId].append(info); + } // 缓存导出顺序即加载顺序, 全量集合与索引同步重建 m_allInfos.append(info); ++count; @@ -462,6 +472,19 @@ CallChainAnalyzer::lookupBest(const QString &name) const return nullptr; } +QList> +CallChainAnalyzer::lookupByObjectId(const QString &objectId) const +{ + // QSharedPointer 到 const 版本的隐式转换无 QList 级快捷路径, 逐个转 + QList> result; + const QList> hits = m_objectIdIndex.value(objectId); + result.reserve(hits.size()); + for (const QSharedPointer &info : hits) { + result.append(info); + } + return result; +} + CallChainAnalyzer::TreeNode CallChainAnalyzer::buildCallTree(const QString &startChineseName, int maxDepth) const { diff --git a/src/utils/callchainanalyzer.h b/src/utils/callchainanalyzer.h index 98998b9..2fda1d1 100644 --- a/src/utils/callchainanalyzer.h +++ b/src/utils/callchainanalyzer.h @@ -62,6 +62,11 @@ public: // 与 resolveNode 使用同一解析入口,供外部还原节点的展示名(如蓝底高亮匹配)。 QSharedPointer lookupBest(const QString &name) const; + // 按功能号(objectId)查找索引中的函数, 返回全部同名功能号候选(一个功能号可能 + // 对应多个接口文件)。元数据接口扫描会排除 cbp/ucbp 目录(moduleGenerationCheck + // 开启时)且同名中文名接口互相覆盖, 部分功能号在元数据中缺失, 代码索引无此限制。 + QList> lookupByObjectId(const QString &objectId) const; + // 构建从 startChineseName 出发的调用链路树(下游:被其调用的函数) TreeNode buildCallTree(const QString &startChineseName, int maxDepth = 32) const; @@ -98,6 +103,9 @@ private: QString m_rootPath; QHash> m_chineseNameIndex; QHash> m_canonicalNameIndex; + // 功能号 -> 函数列表: 一个功能号可能挂在多个接口文件上(不同模块同名 objectId), + // 与 chineseNameIndex 同口径(仅 chineseName 非空的文件), 两条构建路径同步维护 + QHash>> m_objectIdIndex; // 全量函数集合(buildIndex/loadFromCache 两条构建路径的共同底座): // saveToCache 的单一导出事实来源, 天然无重复, 索引哈希则允许同名落选变体缺席 QList> m_allInfos; diff --git a/src/utils/datacache.cpp b/src/utils/datacache.cpp index 491d749..2dcf527 100644 --- a/src/utils/datacache.cpp +++ b/src/utils/datacache.cpp @@ -697,6 +697,9 @@ void DataCache::refreshFunctionCache() m_uf20FuncCacheValid = false; m_uft3FuncCacheValid = false; + // 表查询"表名→中文名"缓存一并失效(同源于本次元数据落库) + m_uf20TableNameMapValid = false; + m_uft3TableNameMapValid = false; // 触发后台重建, 不阻塞主线程; 后续查询在预热完成前按原全表路径兜底 warmupFunctionCacheAsync(Constants::Cache::Uf20DbName); warmupFunctionCacheAsync(Constants::Cache::Uft3DbName); @@ -750,3 +753,79 @@ QList DataCache::searchUFT3Functions(const QStringList& keywords, { return searchFunctions(Constants::Cache::Uft3DbName, keywords, field, exactMatch, keywordsWithoutMatch); } + +// ---- 表查询: 表名→中文名轻量映射(uf2_table / uft3_table) ---- + +// 构建指定库的"表名→中文名"全量轻量映射。 +// data 列为完整表定义 JSON(单行可达数十 KB), 不能 getAllRows 整表搬运后逐行解析, +// 优先在 SQL 层用 json_extract 只提取中文名字段(传输量降为表名+中文名); +// json_extract 不可用(SQL 执行失败)时回退 getAllRows + C++ 逐行解析兜底。 +// 可在任意线程调用: database() 按线程建立专用连接, 与主线程查询互不阻塞 +QMap DataCache::buildTableNameMap(const QString& db, const QString& table, + const QString& cnameField, bool* ok) const +{ + if (ok) *ok = false; + QMap result; + QSqlDatabase conn = database(db, /*createIfMissing=*/false); + if (!conn.isOpen()) { + return result; + } + QSqlQuery query(conn); + // cnameField 为内部常量字面量("cname"/"chineseName"), 非外部输入, 无注入风险 + query.prepare(QString("SELECT name, json_extract(data, '$.%1') FROM %2").arg(cnameField, table)); + if (query.exec()) { + while (query.next()) { + result[query.value(0).toString()] = query.value(1).toString(); + } + if (ok) *ok = true; + return result; + } + LogManager::instance()->logWarning(QString("数据缓存 - json_extract 提取 %1.%2 失败: %3, 回退全表解析") + .arg(db, cnameField, query.lastError().text())); + bool rowsOk = false; + const QMap rows = getAllRows(db, table, &rowsOk); + if (!rowsOk) { + return result; // 库未打开/查询失败: 返回空表交由调用方重试 + } + for (auto it = rows.constBegin(); it != rows.constEnd(); ++it) { + const QJsonObject obj = QJsonDocument::fromJson(it.value().toUtf8()).object(); + result[it.key()] = obj.value(cnameField).toString(); + } + if (ok) *ok = true; + return result; +} + +// 懒加载并缓存指定库的"表名→中文名"映射。首次调用全量构建(表数量远小于接口表, +// 秒级内完成), 之后直接返回内存副本; 构建失败不置有效, 下次查询自动重试, +// 避免空缓存被标有效后表查询永远返回空结果 +QMap DataCache::getUf20TableNameMap() +{ + if (m_uf20TableNameMapValid) { + return m_uf20TableNameMap; + } + bool ok = false; + const QMap map = buildTableNameMap( + Constants::Cache::Uf20DbName, Constants::Cache::TblUf2Table, QStringLiteral("cname"), &ok); + if (ok) { + m_uf20TableNameMap = map; + m_uf20TableNameMapValid = true; + LogManager::instance()->logInfo(QString("数据缓存 - 重建 UF20 表名缓存 %1 条").arg(map.size())); + } + return map; +} + +QMap DataCache::getUft3TableNameMap() +{ + if (m_uft3TableNameMapValid) { + return m_uft3TableNameMap; + } + bool ok = false; + const QMap map = buildTableNameMap( + Constants::Cache::Uft3DbName, Constants::Cache::TblUft3Table, QStringLiteral("chineseName"), &ok); + if (ok) { + m_uft3TableNameMap = map; + m_uft3TableNameMapValid = true; + LogManager::instance()->logInfo(QString("数据缓存 - 重建 UFT3 表名缓存 %1 条").arg(map.size())); + } + return map; +} diff --git a/src/utils/datacache.h b/src/utils/datacache.h index c32373c..8e46067 100644 --- a/src/utils/datacache.h +++ b/src/utils/datacache.h @@ -87,6 +87,15 @@ public: QList searchUFT3Functions(const QStringList& keywords, const QString& field = "", bool exactMatch = false, int* keywordsWithoutMatch = nullptr); + // ---- 表查询 (表名→中文名映射, 从 uf2_table / uft3_table 读取) ---- + // 表名→中文名全量轻量映射: UF20 取 data.cname, 内存表取 data.chineseName。 + // data 列为完整表定义 JSON(单行可达数十 KB), 不能整表搬运后逐行解析, + // SQL 层用 json_extract 只取中文名字段; 首次调用全量构建后内存缓存, + // 元数据落库后随 refreshFunctionCache 一并失效重建。 + // 查询失败(库未打开/SQL 出错)返回空表且不缓存, 下次调用自动重试 + QMap getUf20TableNameMap(); + QMap getUft3TableNameMap(); + // 重新构建功能查询内存缓存(元数据落库后调用, 使后续查询走缓存而非全表扫描) void refreshFunctionCache(); @@ -126,6 +135,11 @@ private: QList searchFunctions(const QString& db, const QStringList& keywords, const QString& field, bool exactMatch, int* keywordsWithoutMatch); + // 构建指定库的"表名→中文名"全量轻量映射(仅库名/表名/中文字段名不同, 成对方法一行转发)。 + // 可在任意线程调用: database() 按线程建立专用连接, 与主线程查询互不阻塞 + QMap buildTableNameMap(const QString& db, const QString& table, + const QString& cnameField, bool* ok = nullptr) const; + // 按线程缓存的连接(QThreadStorage 线程本地存储)。连接只能在创建它的线程中使用, // 线程退出时 ScopedConnection 析构自动 removeDatabase——避免 QtConcurrent 空闲线程 // 回收后 QThread* 地址被系统复用时, 拿到已死线程的脏连接(写入静默失败)并泄漏注册表条目。 @@ -151,6 +165,12 @@ private: mutable bool m_uf20FuncCacheValid = false; mutable bool m_uft3FuncCacheValid = false; + // 表查询"表名→中文名"内存缓存(UF20/UFT3 各一份, 落库后随 refreshFunctionCache 失效重建) + mutable QMap m_uf20TableNameMap; + mutable QMap m_uft3TableNameMap; + mutable bool m_uf20TableNameMapValid = false; + mutable bool m_uft3TableNameMapValid = false; + // 功能缓存的在途后台预热任务(结果暂存 QFuture, 由主线程 cachedFunctions 接管, // 共享成员的读写仍全部发生在主线程, 无需额外加锁) mutable QFuture> m_uf20WarmupFuture; diff --git a/src/utils/jsonfilemonitor.cpp b/src/utils/jsonfilemonitor.cpp index ca05cf0..31a7a90 100644 --- a/src/utils/jsonfilemonitor.cpp +++ b/src/utils/jsonfilemonitor.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "logmanager.h" JsonFileMonitor::JsonFileMonitor(QObject *parent) @@ -97,6 +98,10 @@ void JsonFileMonitor::endSave() // 以保存后的新 mtime 为基准(writeCustJsonOrdered 同步写完才返回), // 否则保存后的 mtime > 旧基准会误触发一次重载 m_lastMtime = currentMtime(); + // 开启保存余波静默窗: 替换文件后的目录事件风暴/杀软扫描 touch 会让 mtime + // 在基准之后继续漂移, 曾被 onReloadTimeout 误判为外部修改引发页面重建两轮 + m_quietUntilMs = QDateTime::currentMSecsSinceEpoch() + + Constants::Timings::JsonQuietWindowMs; refreshWatch(); } @@ -121,6 +126,13 @@ void JsonFileMonitor::onDirChanged(const QString &path) void JsonFileMonitor::onReloadTimeout() { + // 保存余波静默期: 挂接失败重试/杀软扫描 touch 造成的 mtime 漂移属于保存本身, + // 只刷新基准并补挂监控, 不触发重载; 静默期后恢复正常外部修改判定 + if (QDateTime::currentMSecsSinceEpoch() < m_quietUntilMs) { + m_lastMtime = currentMtime(); + refreshWatch(); + return; + } const qint64 mtime = currentMtime(); if (mtime <= 0) { refreshWatch(); diff --git a/src/utils/jsonfilemonitor.h b/src/utils/jsonfilemonitor.h index 72147b2..83bb91a 100644 --- a/src/utils/jsonfilemonitor.h +++ b/src/utils/jsonfilemonitor.h @@ -15,6 +15,9 @@ // 3. mtime 基准过滤:自身写入(保存)引发的 mtime 变化不触发重载 // 4. 保存窗口语义:beginSaveWindow()/endSave() 包住保存动作, // 窗口期内监控事件直接忽略;窗口结束后 m_saveSeq 代数立即失效 +// 5. 保存余波静默窗:endSave 后短窗内(QuietWindowMs) QSaveFile 替换目标文件 +// 引发的目录事件风暴/杀软扫描 touch 造成的 mtime 漂移只刷新基准不重载, +// 防止自身保存被误判为外部修改导致页面重建两轮 // // 用法(页面侧): // m_monitor = new JsonFileMonitor(this); @@ -61,6 +64,7 @@ private: qint64 m_lastMtime; // mtime 基准:<= 基准的变化视为自身写入回环 bool m_isSaving; // 保存窗口期内为 true quint32 m_saveSeq; // 保存代数:begin/end 各 +1,使窗口内事件过期 + qint64 m_quietUntilMs = 0; // 保存余波静默窗截止时刻(epoch ms), 期间 mtime 漂移只刷基准 }; #endif // JSONFILEMONITOR_H