更新使用手册;修复滚动条问题;新增表映射关系查询
This commit is contained in:
@@ -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 \
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+52
-4
@@ -4,7 +4,7 @@
|
||||
| --- | --- |
|
||||
| 软件名称 | Uft30ChangeCode(UF20 → UFT3 转码工具) |
|
||||
| 手册版本 | V1.6.0 |
|
||||
| 更新日期 | 2026-09-14 |
|
||||
| 更新日期 | 2026-10-02 |
|
||||
| 适用对象 | 使用本工具进行代码转码的开发/维护人员 |
|
||||
|
||||
---
|
||||
@@ -51,10 +51,14 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
|
||||
| 设置(底部) | 项目环境路径与主题配置 |
|
||||
| 帮助 / 关于(底部) | 在线帮助、版本信息 |
|
||||
|
||||

|
||||
|
||||
## 5. 功能操作详解
|
||||
|
||||
### 5.1 元数据更新(首次使用必做)
|
||||
|
||||

|
||||
|
||||
**前提**:元数据更新按"设置"页配置的项目环境路径扫描源码,请先完成 §5.5 的路径配置。
|
||||
|
||||
**操作步骤:**
|
||||
@@ -84,6 +88,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
|
||||
|
||||
##### 5.2.1.1 全量转码
|
||||
|
||||

|
||||
|
||||
**说明**:全量转码仅支持 **LS_ 开头**的功能("内存交易"改造范围),按清单对功能进行整体转码。清单保存在 `cust.json` 的 `convert.dir` 节点(见 §6.1)。
|
||||
|
||||
**表格列**:`UF20函数名` | `是否存在` | `核心` | `是否柜面接口`(**双击函数名所在行**可在弹窗中修改函数名)。
|
||||
@@ -105,6 +111,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
|
||||
|
||||
##### 5.2.1.2 按需转码
|
||||
|
||||

|
||||
|
||||
**说明**:按需转码针对**非 LS 功能**转码,支持**单文件转码**与**递归转码**两种模式,转码范围不包含核心文件、module 模块文件。
|
||||
|
||||
- **非递归模式**:仅对目标文件本身执行转码,不会处理该文件内调用的函数;
|
||||
@@ -132,6 +140,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
|
||||
|
||||
#### 5.2.2 代码检索
|
||||
|
||||

|
||||
|
||||
**说明**:代码检索用于查看功能的**调用链路**——该功能调用了哪些下游函数、被哪些上游功能调用,用于转码前评估影响范围、转码后核对结果。
|
||||
|
||||
页面顶部 Tab 切换 **转码业务** / **非转码业务** 两种模式,两种模式各自维护检索路径与索引,互不干扰。
|
||||
@@ -168,6 +178,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
|
||||
|
||||
#### 5.3.1 UF20 功能查询
|
||||
|
||||

|
||||
|
||||
**操作步骤:**
|
||||
|
||||
1. 进入 **UF20功能查询** 页;
|
||||
@@ -184,16 +196,42 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
|
||||
|
||||
#### 5.3.2 UFT3 功能查询
|
||||
|
||||

|
||||
|
||||
- 界面与 UF20 功能查询一致(函数名 / 功能号检索、**批量关键词**、精确匹配),结果列为 `函数名`、`英文名`、`功能号`,查询提示同样在页面底部提示栏反馈;
|
||||
- 仅用于查询核对 UFT3 功能元数据,**双击/右键不加入转码清单**;
|
||||
- 代码检索中"原代码已支持"行前的功能号即来自 UFT3 元数据。
|
||||
|
||||
#### 5.3.3 UF20 & 内存表映射查询
|
||||
|
||||

|
||||
|
||||
检索 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 表替换
|
||||
|
||||

|
||||
|
||||
维护 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 函数名的映射,操作方式与表替换完全相同。
|
||||

|
||||
|
||||
维护 UF20 函数名 → UFT3 函数名的映射,操作方式与表替换相同,但**不涉及** `table_mapping.json` 自动同步(该同步为表替换独有)。
|
||||
|
||||
#### 5.4.3 不转码功能函数
|
||||
|
||||

|
||||
|
||||
维护**不参与函数名替换**的功能函数清单,对应 `cust.json` 顶层键 `noChangeFullNameCode`(字符串数组)。转码时,清单内的功能函数名保持原样,不做 UF20 → UFT3 全名替换。
|
||||
|
||||
**操作步骤:**
|
||||
@@ -226,6 +272,8 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
|
||||
|
||||
### 5.5 系统设置(底部"设置")
|
||||
|
||||

|
||||
|
||||
**环境配置**:
|
||||
|
||||
| 配置项 | 说明 |
|
||||
@@ -264,7 +312,7 @@ Uft30ChangeCode 是一款桌面端 UF20 → UFT3 代码转码工具,主要提
|
||||
| `functionReplace` | 对象 `{UF20函数名: UFT3函数名}` | 函数替换映射 |
|
||||
| `noChangeFullNameCode` | 字符串数组 | 不转码功能函数清单,转码时跳过这些函数名的替换 |
|
||||
|
||||
`convert.dir` 元素字段说明(详见 `docs/转码配置格式契约.md`):
|
||||
`convert.dir` 元素字段说明(详见 `docs/详细设计文档.md` §7.5):
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
|
||||
+197
-5
@@ -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 列(内存表名),<br/>任一为空即无完整映射关系,tip 警告
|
||||
S->>M: requestAddMapping(uf20Name, memName)
|
||||
M->>T: appendPairIfAbsent(uf20Name, memName)
|
||||
Note over T: trimmed 口径判重(与添加/收集同口径);<br/>已存在返回提示(含当前映射值,不覆盖);<br/>新增入模型置脏后立即落盘(自动保存)
|
||||
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 = <UFT30路径>\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/`(解析器组) |
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<FunctionInfo> results = cache->searchUFT3Functions(raw, Constants::FunctionSearch::FieldFunctionNo, true);
|
||||
if (results.isEmpty() || results.first().eName.isEmpty()) {
|
||||
QMessageBox::information(this, "提示",
|
||||
QString("未找到功能号:%1 对应的函数").arg(raw));
|
||||
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;
|
||||
}
|
||||
target = results.first().eName;
|
||||
const QList<QSharedPointer<const CallChainAnalyzer::FunctionInfo>> byId =
|
||||
currentAnalyzer()->lookupByObjectId(raw);
|
||||
if (byId.isEmpty()) {
|
||||
QMessageBox::information(this, "提示",
|
||||
QString("未找到功能号:%1 对应的函数\n"
|
||||
"请确认该功能号属于 UF30 工程接口,且元数据已更新、"
|
||||
"代码检索路径正确并已刷新索引。").arg(raw));
|
||||
return;
|
||||
}
|
||||
// 同功能号多接口时取第一个候选(均在当前检索目录内, 皆可展示调用链)
|
||||
target = byId.first()->chineseName;
|
||||
}
|
||||
}
|
||||
|
||||
runQuery(target);
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include <QPainter>
|
||||
#include <QShowEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
#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();
|
||||
|
||||
@@ -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<QString, QString> overrideRaw = collectMappingData(custJson, errorMessage);
|
||||
if (!errorMessage.isEmpty()) {
|
||||
// 子类校验并回填数据: 校验失败不落盘
|
||||
QString validateError;
|
||||
const QHash<QString, QString> 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()
|
||||
|
||||
@@ -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(键名→格式化原文);
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
#include "tablemappingpage.h"
|
||||
#include "utils/Constants.h"
|
||||
#include "src/utils/Constants.h"
|
||||
#include "src/utils/logmanager.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonValue>
|
||||
#include <QStandardItem>
|
||||
|
||||
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<QString, QString> 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<Member> 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<QString> 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()));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -73,6 +73,35 @@ QHash<QString, QString> 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<QStandardItem*> 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);
|
||||
|
||||
@@ -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<QString, QString> collectMappingData(QJsonObject &custJson,
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
#include "tablesearchpage.h"
|
||||
#include "src/utils/Constants.h"
|
||||
#include "src/utils/datacache.h"
|
||||
#include "src/utils/logmanager.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QHeaderView>
|
||||
#include <QStandardItem>
|
||||
#include <QRegularExpression>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <algorithm>
|
||||
#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<QPair<QString, QString>> TableSearchPage::loadTableMapping(bool* readOk,
|
||||
QString* errorOut) const
|
||||
{
|
||||
if (readOk) *readOk = false;
|
||||
if (errorOut) errorOut->clear();
|
||||
QList<QPair<QString, QString>> 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<QString, QString> uf20Names = DataCache::instance()->getUf20TableNameMap();
|
||||
const QMap<QString, QString> memNames = DataCache::instance()->getUft3TableNameMap();
|
||||
if (uf20Names.isEmpty() && memNames.isEmpty()) {
|
||||
if (m_tipLabel) {
|
||||
m_tipLabel->setText("⚠️ 缓存库中暂无表数据,请先在「元数据更新」页面执行更新");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool mappingReadOk = false;
|
||||
QString mappingError;
|
||||
const QList<QPair<QString, QString>> mapping = loadTableMapping(&mappingReadOk, &mappingError);
|
||||
// 映射文件是本页核心数据源: 读取/解析失败时醒目报错并停止检索,
|
||||
// 不再降级展示单侧结果(否则表现为"有表名却查不到映射", 极具误导性)
|
||||
if (!mappingReadOk) {
|
||||
if (m_tipLabel) {
|
||||
m_tipLabel->setText("⚠️ " + mappingError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 单行结果: 四列与页面列定义一致, 任一表名侧为空即"无映射关系"行
|
||||
struct TableRow {
|
||||
QString uf20Name, uf20Cname, memName, memCname;
|
||||
};
|
||||
QList<TableRow> rows;
|
||||
// 已覆盖表名集合: 映射整体行里的表不再补单侧行, 防止同一表重复出现
|
||||
QSet<QString> coveredUf20;
|
||||
QSet<QString> coveredMem;
|
||||
// 已输出的映射对(以换行分隔拼接): 多关键词可能重复命中同一映射对, 去重
|
||||
QSet<QString> 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<QStandardItem*> 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);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#ifndef TABLESEARCHPAGE_H
|
||||
#define TABLESEARCHPAGE_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QStandardItemModel>
|
||||
#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<QPair<QString, QString>> loadTableMapping(bool* readOk,
|
||||
QString* errorOut = nullptr) const;
|
||||
|
||||
ElaSearchEdit *m_searchEdit;
|
||||
ElaTableView *m_resultTable;
|
||||
QStandardItemModel *m_tableModel;
|
||||
QLabel *m_tipLabel;
|
||||
bool m_exactMatch;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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<QSharedPointer<const CallChainAnalyzer::FunctionInfo>>
|
||||
CallChainAnalyzer::lookupByObjectId(const QString &objectId) const
|
||||
{
|
||||
// QSharedPointer<FunctionInfo> 到 const 版本的隐式转换无 QList 级快捷路径, 逐个转
|
||||
QList<QSharedPointer<const FunctionInfo>> result;
|
||||
const QList<QSharedPointer<FunctionInfo>> hits = m_objectIdIndex.value(objectId);
|
||||
result.reserve(hits.size());
|
||||
for (const QSharedPointer<FunctionInfo> &info : hits) {
|
||||
result.append(info);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
CallChainAnalyzer::TreeNode
|
||||
CallChainAnalyzer::buildCallTree(const QString &startChineseName, int maxDepth) const
|
||||
{
|
||||
|
||||
@@ -62,6 +62,11 @@ public:
|
||||
// 与 resolveNode 使用同一解析入口,供外部还原节点的展示名(如蓝底高亮匹配)。
|
||||
QSharedPointer<const FunctionInfo> lookupBest(const QString &name) const;
|
||||
|
||||
// 按功能号(objectId)查找索引中的函数, 返回全部同名功能号候选(一个功能号可能
|
||||
// 对应多个接口文件)。元数据接口扫描会排除 cbp/ucbp 目录(moduleGenerationCheck
|
||||
// 开启时)且同名中文名接口互相覆盖, 部分功能号在元数据中缺失, 代码索引无此限制。
|
||||
QList<QSharedPointer<const FunctionInfo>> lookupByObjectId(const QString &objectId) const;
|
||||
|
||||
// 构建从 startChineseName 出发的调用链路树(下游:被其调用的函数)
|
||||
TreeNode buildCallTree(const QString &startChineseName, int maxDepth = 32) const;
|
||||
|
||||
@@ -98,6 +103,9 @@ private:
|
||||
QString m_rootPath;
|
||||
QHash<QString, QSharedPointer<FunctionInfo>> m_chineseNameIndex;
|
||||
QHash<QString, QSharedPointer<FunctionInfo>> m_canonicalNameIndex;
|
||||
// 功能号 -> 函数列表: 一个功能号可能挂在多个接口文件上(不同模块同名 objectId),
|
||||
// 与 chineseNameIndex 同口径(仅 chineseName 非空的文件), 两条构建路径同步维护
|
||||
QHash<QString, QList<QSharedPointer<FunctionInfo>>> m_objectIdIndex;
|
||||
// 全量函数集合(buildIndex/loadFromCache 两条构建路径的共同底座):
|
||||
// saveToCache 的单一导出事实来源, 天然无重复, 索引哈希则允许同名落选变体缺席
|
||||
QList<QSharedPointer<FunctionInfo>> m_allInfos;
|
||||
|
||||
@@ -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<FunctionInfo> 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<QString, QString> DataCache::buildTableNameMap(const QString& db, const QString& table,
|
||||
const QString& cnameField, bool* ok) const
|
||||
{
|
||||
if (ok) *ok = false;
|
||||
QMap<QString, QString> 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<QString, QString> 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<QString, QString> DataCache::getUf20TableNameMap()
|
||||
{
|
||||
if (m_uf20TableNameMapValid) {
|
||||
return m_uf20TableNameMap;
|
||||
}
|
||||
bool ok = false;
|
||||
const QMap<QString, QString> 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<QString, QString> DataCache::getUft3TableNameMap()
|
||||
{
|
||||
if (m_uft3TableNameMapValid) {
|
||||
return m_uft3TableNameMap;
|
||||
}
|
||||
bool ok = false;
|
||||
const QMap<QString, QString> 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;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,15 @@ public:
|
||||
QList<FunctionInfo> 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<QString, QString> getUf20TableNameMap();
|
||||
QMap<QString, QString> getUft3TableNameMap();
|
||||
|
||||
// 重新构建功能查询内存缓存(元数据落库后调用, 使后续查询走缓存而非全表扫描)
|
||||
void refreshFunctionCache();
|
||||
|
||||
@@ -126,6 +135,11 @@ private:
|
||||
QList<FunctionInfo> searchFunctions(const QString& db, const QStringList& keywords,
|
||||
const QString& field, bool exactMatch, int* keywordsWithoutMatch);
|
||||
|
||||
// 构建指定库的"表名→中文名"全量轻量映射(仅库名/表名/中文字段名不同, 成对方法一行转发)。
|
||||
// 可在任意线程调用: database() 按线程建立专用连接, 与主线程查询互不阻塞
|
||||
QMap<QString, QString> 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<QString, QString> m_uf20TableNameMap;
|
||||
mutable QMap<QString, QString> m_uft3TableNameMap;
|
||||
mutable bool m_uf20TableNameMapValid = false;
|
||||
mutable bool m_uft3TableNameMapValid = false;
|
||||
|
||||
// 功能缓存的在途后台预热任务(结果暂存 QFuture, 由主线程 cachedFunctions 接管,
|
||||
// 共享成员的读写仍全部发生在主线程, 无需额外加锁)
|
||||
mutable QFuture<QList<FunctionInfo>> m_uf20WarmupFuture;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QDateTime>
|
||||
#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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user