Преглед изворни кода

现场修改的代码同步到行内

jiayongqiang пре 2 недеља
родитељ
комит
23e1fa9b9c
27 измењених фајлова са 590 додато и 174 уклоњено
  1. 3 1
      agent/config.ini
  2. BIN
      agent/dist/agent-0.1.8.3-py3-none-any.whl
  3. BIN
      agent/dist/agent-0.1.8.3-py3-none-any.whl.zip
  4. BIN
      agent/dist/agent-0.1.8.3.tar.gz
  5. BIN
      agent/dist/dist.zip
  6. BIN
      agent/logs/aitagging-app.2026-05-22_10-40-11_734541.log.zip
  7. 92 64
      agent/src/agent/agent.py
  8. 101 37
      agent/src/agent/api_outter.py
  9. 43 13
      agent/src/agent/core/es.py
  10. 4 2
      agent/src/agent/main.py
  11. 5 0
      agent/tests/test_config.py
  12. 36 0
      agent/tests/test_es.py
  13. 32 29
      agent/tests/test_reg.py
  14. 6 0
      agent/tests/test_reg1.py
  15. 145 0
      agent/tests/test_rerank.py
  16. 8 0
      agent/tests/test_similar.py
  17. 3 0
      agent/tests/test_split.py
  18. 3 3
      agent/tests/test_sync_category.py
  19. 13 7
      agent/tests/test_tagging.py
  20. 0 1
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/controller/AitagTagLogController.java
  21. 9 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/dto/TagLogDto.java
  22. 3 2
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/entity/AitagTagLogEntity.java
  23. 1 1
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/vo/SmartTaggingResultVo.java
  24. 3 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/vo/fastapivo/AiTaggingQueryResponseVo.java
  25. 60 12
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/impl/AitagTagLogServiceImpl.java
  26. 6 2
      server/yusp-tagging-core/src/main/resources/mapper/AitagTagLogMapper.xml
  27. 14 0
      server/yusp-tagging-starter/src/test/java/cn/com/yusys/yusp/TestManagerAuth.java

+ 3 - 1
agent/config.ini

@@ -14,7 +14,7 @@ temperature = 0.2
 base_url = http://172.16.40.16:20001/compatible-mode/v1
 api_key = 
 max_retries = 3
-timeout = 30
+timeout = 60
 
 [embedding]
 model = Qwen3-Embedding-8B
@@ -32,6 +32,8 @@ password = 123456
 port=9876
 concurrence=1
 esb_callback=http://xxxxx:xxxx/api/callback
+top_n=10
+breaker=false
 
 [logging]
 log_path= logs/aitagging-app.log

BIN
agent/dist/agent-0.1.8.3-py3-none-any.whl


BIN
agent/dist/agent-0.1.8.3-py3-none-any.whl.zip


BIN
agent/dist/agent-0.1.8.3.tar.gz


BIN
agent/dist/dist.zip


BIN
agent/logs/aitagging-app.2026-05-22_10-40-11_734541.log.zip


+ 92 - 64
agent/src/agent/agent.py

@@ -1,4 +1,5 @@
 from langchain.chat_models import init_chat_model
+from langchain.messages import HumanMessage, SystemMessage
 from langchain.agents import create_agent
 from pydantic import BaseModel,Field
 import json
@@ -6,20 +7,18 @@ from agent.core.config import get_config_path
 from agent.logger import logger
 import uuid
 from datetime import datetime
-from agent.logger import logger
+from langchain.agents.middleware import ModelRetryMiddleware
+
 config = get_config_path()
 
+breaker = config['app'].getboolean("breaker", False)
 base_url = config['llm']['base_url']
 api_key_env_var = config['llm']['api_key']
 temperature = config['llm']['temperature']
 model = config['llm']['model']
 # max_retries和timeout也从配置文件中读取,增加了默认值,以防止配置文件中缺失这两个参数导致的错误
-max_retries = config['llm']['max_retries']
-if max_retries is None:
-    max_retries = 3  # 默认重试次数
-timeout = config['llm']['timeout']
-if timeout is None:
-    timeout = 30  # 默认超时时间(秒)
+max_retries = config['llm'].getint("max_retries", 2)
+timeout = config['llm'].getint("timeout", 60)
 logger.info(f"model_name:{model}, base_url:{base_url}")
 llm = init_chat_model(
     model_provider="openai", 
@@ -28,8 +27,7 @@ llm = init_chat_model(
     base_url=base_url,
     temperature= temperature,
     extra_body={"enable_thinking": False},
-    max_retries=max_retries,
-    # timeout=timeout
+    timeout=timeout
 )
 
 class Lable(BaseModel):
@@ -52,69 +50,99 @@ class Lables(BaseModel):
     labels: list[Lable] = Field(description="List of optimized labels after reflection.")
     
 def reflect_check_sync(context: str,is_marine: bool, labels: list[str]):
-    """同步版本的 reflect_check,用于后台任务"""
+    system_prompt = f"""
+你是一位经验丰富的银行贷款打标员,负责基于贷款信息文本内容[context],对候选标签[labels]进行相关性判断和筛选。
+
+打标规则:
+1. 沿海行社标识参数为is_marine
+    - is_marine=1表示是沿海行社,如果候选标签中包含海洋体系的标签,则在判定时可以适当放宽对海洋标签的语义关联要求;
+    - is_marine=0表示不是沿海行社,判定时忽略此参数
+2. 不区分职业、投向、用途等属性的界限,凡包含相关内容即参与打标
+    """
     agent = create_agent(
         model = llm, 
-        response_format=Lables
+        system_prompt=system_prompt,
+        response_format=Lables,
+        middleware=[
+            ModelRetryMiddleware(
+                max_retries=max_retries,
+                backoff_factor=2.0,
+                initial_delay=1.0,
+                max_delay=30
+            ),
+        ]
     )
-    prompt = f"""
-    一、角色与职责
-你是一名银行智能标签平台中的语义判定模型,负责基于贷款信息文本内容,对候选标签进行相关性判断和筛选。
-你的核心职责是从“贷款打标”的视角,评估文本中所描述的资产、项目、合同、用途或相关经济活动,是否与标签定义所描述的产业活动在语义上存在明确、合理、可引用的关联。
-
-二、基本工作原则
-1. 仅依据所提供的文本内容进行判断,不得引入外部资料。
-2. 不要求文本构成完整的行业介绍或业务说明。
-3. 不要求覆盖标签定义的全部要素。
-4. 所有结论必须能够回溯到具体文本证据。
-5. “语义不矛盾”不等于“语义有关联”。仅凭无法排除可能性就推荐标签属于错误判定。
-6. 若存在合理支持但信息不完整,可判定命中并通过不确定性说明体现风险,但绝不允许在没有任何语义连接点的情况下强行命中。
-7. 转贷操作词无效原则:如果文本中出现“无还本续贷”“借新还旧”“二押”“收回再贷”“压降转贷”等表述,这些属于贷款操作形式,其本身不能作为任何产业标签关联的实质依据。判定时必须忽略这些操作词,重点分析文本中括号内补充的内容,或上下文中的“投向”“用途”等字段里的实质性描述。若实质内容缺失,无法判断产业关联,则所有标签均判false。此原则适用于所有产业标签体系。
-
-三、关键判定标准(必须遵守)
-1. 关联性必须具体:文本中必须出现某个实体、行为、商品、服务、场景或用途,能够与标签定义中的产业活动形成直接或可解释的间接关联。
-2. 常识否定原则:若文本所描述的活动具有明显与该标签所属产业体系核心特征相悖的常识性冲突(例如在海洋经济体系中,茶叶种植等典型内陆活动不得推荐涉海标签),则不得推荐该标签。
-3. 排除情形必须严格执行:若文本内容明确符合标签定义中的排除说明,应判定为不命中;若标签定义无明确排除,但常识上显著冲突,也应判定为不命中。
-
-四、海洋经济体系专属规则
-本部分规则仅适用于标签所属产业体系为“海洋经济”的标签。体系归属可通过标签定义中的体系名称或标签路径前缀(如"海洋产业"/"海洋科研教育"/"海洋公共管理服务"/"海洋上游相关产业"/"海洋下游相关产业"等)识别。对于养老产业、科技金融等其他体系的标签,请忽略本部分所有内容,直接进入第五步进行标准语义判定。
-
-当前贷款发起行社的沿海标识为 {is_marine} (1=沿海行社,0=非沿海行社),该参数仅在处理海洋经济体系标签时生效。
-
-1. 教育场景排除规则
-如果文本中的用途涉及“学费”“住宿费”“培训费”“杂费”等教育类支出,且投向描述为“普通高等教育”“大学”“学院”等未明确指向海洋相关专业的学历教育:
-a. 绝对不得推荐“海洋中等职业教育”“海洋社会人文科学研究”及其他海洋教育/科研类标签。
-b. 除非文本中明确出现了“海洋”“航海”“水产”等涉海专业名称或科研机构,否则视为不命中。
-
-2. 沿海行社宽泛匹配规则
-当 {is_marine} = 1 时,启用以下扩展推理授权:
-a. 对于文本中出现的住宿、餐饮、装修、仓储、物流、运输、批发零售等具有服务或流通属性的行业性描述,允许在语义上进行适度延伸,将标签定义中最相近的涉海服务类标签(如“涉海旅游消费”“涉海物流”“涉海批发经营”等)纳入候选判断。
-b. 进行此类延伸时,文本中仍必须存在可关联的实体或行为,不得凭空推荐。延伸推荐的理由中应注明“基于沿海区域宽泛匹配推断,需人工确认”。
-当 {is_marine} = 0 时,禁用上述扩展推理,严格按照标准语义关联进行判断,不允许任何无直接证据的延伸推荐。
-
-五、候选标签
-以下是本次判定所依据的产业标签定义信息:
-    {labels}
-
-六、贷款信息
-    {context}   
+#     prompt = f"""
+#     一、角色与职责
+# 你是一名银行智能标签平台中的语义判定模型,负责基于贷款信息文本内容,对候选标签进行相关性判断和筛选。
+# 你的核心职责是从“贷款打标”的视角,评估文本中所描述的资产、项目、合同、用途或相关经济活动,是否与标签定义所描述的产业活动在语义上存在明确、合理、可引用的关联。
+
+# 二、基本工作原则
+# 1. 仅依据所提供的文本内容进行判断,不得引入外部资料。
+# 2. 不要求文本构成完整的行业介绍或业务说明。
+# 3. 不要求覆盖标签定义的全部要素。
+# 4. 所有结论必须能够回溯到具体文本证据。
+# 5. “语义不矛盾”不等于“语义有关联”。仅凭无法排除可能性就推荐标签属于错误判定。
+# 6. 若存在合理支持但信息不完整,可判定命中并通过不确定性说明体现风险,但绝不允许在没有任何语义连接点的情况下强行命中。
+# 7. 转贷操作词无效原则:如果文本中出现“无还本续贷”“借新还旧”“二押”“收回再贷”“压降转贷”等表述,这些属于贷款操作形式,其本身不能作为任何产业标签关联的实质依据。判定时必须忽略这些操作词,重点分析文本中括号内补充的内容,或上下文中的“投向”“用途”等字段里的实质性描述。若实质内容缺失,无法判断产业关联,则所有标签均判false。此原则适用于所有产业标签体系。
+
+# 三、关键判定标准(必须遵守)
+# 1. 关联性必须具体:文本中必须出现某个实体、行为、商品、服务、场景或用途,能够与标签定义中的产业活动形成直接或可解释的间接关联。
+# 2. 常识否定原则:若文本所描述的活动具有明显与该标签所属产业体系核心特征相悖的常识性冲突(例如在海洋经济体系中,茶叶种植等典型内陆活动不得推荐涉海标签),则不得推荐该标签。
+# 3. 排除情形必须严格执行:若文本内容明确符合标签定义中的排除说明,应判定为不命中;若标签定义无明确排除,但常识上显著冲突,也应判定为不命中。
+
+# 四、海洋经济体系专属规则
+# 本部分规则仅适用于标签所属产业体系为“海洋经济”的标签。体系归属可通过标签定义中的体系名称或标签路径前缀(如"海洋产业"/"海洋科研教育"/"海洋公共管理服务"/"海洋上游相关产业"/"海洋下游相关产业"等)识别。对于养老产业、科技金融等其他体系的标签,请忽略本部分所有内容,直接进入第五步进行标准语义判定。
+
+# 当前贷款发起行社的沿海标识为 {is_marine} (1=沿海行社,0=非沿海行社),该参数仅在处理海洋经济体系标签时生效。
 
+# 1. 教育场景排除规则
+# 如果文本中的用途涉及“学费”“住宿费”“培训费”“杂费”等教育类支出,且投向描述为“普通高等教育”“大学”“学院”等未明确指向海洋相关专业的学历教育:
+# a. 绝对不得推荐“海洋中等职业教育”“海洋社会人文科学研究”及其他海洋教育/科研类标签。
+# b. 除非文本中明确出现了“海洋”“航海”“水产”等涉海专业名称或科研机构,否则视为不命中。
+
+# 2. 沿海行社宽泛匹配规则
+# 当 {is_marine} = 1 时,启用以下扩展推理授权:
+# a. 对于文本中出现的住宿、餐饮、装修、仓储、物流、运输、批发零售等具有服务或流通属性的行业性描述,允许在语义上进行适度延伸,将标签定义中最相近的涉海服务类标签(如“涉海旅游消费”“涉海物流”“涉海批发经营”等)纳入候选判断。
+# b. 进行此类延伸时,文本中仍必须存在可关联的实体或行为,不得凭空推荐。延伸推荐的理由中应注明“基于沿海区域宽泛匹配推断,需人工确认”。
+# 当 {is_marine} = 0 时,禁用上述扩展推理,严格按照标准语义关联进行判断,不允许任何无直接证据的延伸推荐。
+
+# 五、候选标签
+# 以下是本次判定所依据的产业标签定义信息:
+#     {labels}
+
+# 六、贷款信息
+#     {context}   
+
+#     """
+
+    user_message = f"""
+is_marine: {1 if is_marine else 0}
+
+context: {context}
+
+labels: {json.dumps(labels, ensure_ascii=False)}
     """
     uid = uuid.uuid4().hex
     l1 = datetime.now().isoformat()
     logger.info(f"Starting reflection check with uuid {uid}. timestamp: {l1}")
-    response = agent.invoke({
-        "messages": [{"role":"user","content": prompt}]
-    },context = {})
-    result = response["structured_response"]
-    logger.info(f"{context} LLM result: {result}")
-    # 只保留passr为true的标签,并将结果转换为字典列表格式
-    result =  [r.dict() for r in result.labels ]
-    result = json.dumps(result, ensure_ascii=False)
-    l2 = datetime.now().isoformat()
-    logger.info(f"Reflection check completed with uuid {uid}. timestamp: {l2}, consuming {(datetime.fromisoformat(l2) - datetime.fromisoformat(l1)).total_seconds()} seconds.")
-
-    return result
+    if breaker:
+        logger.info(f"Breaker is enabled. Returning all labels as is without reflection check. uuid {uid}")
+        result =  [ {"id": label["id"], "tag_code": label["tag_code"], "tag_name": label["tag_name"], "tag_path": label["tag_path"], "category_id": label["category_id"], "desc": "Breaker enabled, no reflection check performed.", "passr": True} for  label in labels]
+        result = json.dumps(result, ensure_ascii=False)
+    else:
+        response = agent.invoke({
+            "messages": [HumanMessage(content=user_message)]
+        },context = {})
+        result = response["structured_response"]
+        logger.info(f"{context} LLM result: {result}")
+        # 只保留passr为true的标签,并将结果转换为字典列表格式
+        result =  [r.dict() for r in result.labels ]
+        result = json.dumps(result, ensure_ascii=False)
+        l2 = datetime.now().isoformat()
+        logger.info(f"Reflection check completed with uuid {uid}. timestamp: {l2}, consuming {(datetime.fromisoformat(l2) - datetime.fromisoformat(l1)).total_seconds()} seconds.")
+
+    return result, system_prompt+user_message
 
 class GenerateReg(BaseModel):
     """ 大模型生成的正则表达式 """

+ 101 - 37
agent/src/agent/api_outter.py

@@ -26,7 +26,7 @@ CONCURRENCE = int(config['app']['concurrence'])
 # background_semaphore = threading.BoundedSemaphore(CONCURRENCE)
 executor = ThreadPoolExecutor(max_workers=CONCURRENCE)
 ESB_CALLBACK = config['app']['esb_callback']
-
+TOP_N = int(config['app'].get("top_n",5))
 router = APIRouter(prefix="/v1", tags=["AI Tagging"])
 
 class TaggingRequest(BaseModel):
@@ -61,20 +61,32 @@ class TaggingRequest(BaseModel):
             return None
         return str(v)
 
+def reg_match(query:str, reglist:list[any]):
+    result = []
+    for id,reg in reglist:
+        try:
+            if re.search(reg, query):
+                result.append(id)
+        except re.error:
+            continue
+    return result
+
 def execute_reg(log_id:str,tag_category_id:str,phrase: str)-> list:
+    logger.info(f"[{log_id}] Regex filtering start!")
     sql = f"""select 
                     tti.id,
                     tti.reg
                     from aitag_tag_info tti left join aitag_tag_category  ttc 
                     on tti.category_id = ttc.id 
                     where ttc.is_delete=0 and tti.is_delete=0 and ttc.state =  0 and tti.state = 0 and tti.tag_level = ttc.visibility_level
-                    and '{phrase}' ~ tti.reg and tti.reg is not null and length(tti.reg) > 0
+                    and tti.reg is not null and length(tti.reg) > 0  
                     """    
     if tag_category_id:
         sql += f""" and ttc.id = '{tag_category_id}'"""
     labels = dao.query(sql)
+    result = reg_match(phrase, labels)
     # 循环调用reg匹配phrase,匹配成功则返回标签id
-    result = [label[0] for label in labels]
+    # result = [label[0] for label in labels]
     if result and len(result) > 0:
         dao.execute(
                 """UPDATE aitag_tag_log SET reg_result = %s,tagging_channel = %s WHERE id = %s""",
@@ -83,11 +95,18 @@ def execute_reg(log_id:str,tag_category_id:str,phrase: str)-> list:
     logger.info(f"[{log_id}] Regex filtering result: {result}")
     return result
 
-def vector_similarity_search(log_id:str,phrase: str)-> list:
+def vector_similarity_search(log_id:str,phrase: str,tag_ids:list[str]=None)-> list:
     logger.info("Starting vector similarity search...")
     # 这里应该调用向量数据库进行相似度检索,返回相关标签id列表
+    l1 = time.time()
     query = get_embeddings([phrase])[0]
-    results = bm25_vector_search(phrase,query)
+    l2 = time.time()
+    logger.info(f"[{log_id}] Vector embedding time: {l2-l1}")
+    l3 = time.time()
+    rrf_score_threshold = float(config['es'].get('rrf_score_threshold',0.016))
+    results = bm25_vector_search(phrase,query,tag_ids=tag_ids,rrf_score_threshold=rrf_score_threshold)
+    l4 = time.time()
+    logger.info(f"[{log_id}] Vector search time: {l4-l3}")
     dao.execute(
             """UPDATE aitag_tag_log SET tagging_channel = %s WHERE id = %s""",
             (TAGGING_CHANNEL.VECTOR.value, log_id)    
@@ -104,15 +123,15 @@ def init_tag_log(request: TaggingRequest):
     # 业务编号如果以test开头,则tag_scope = 1,否则都是0
     tag_scope = 1 if request.business_attr.startswith("test") else 0
     dao.execute(
-        """INSERT INTO aitag_tag_log (id,app_id, insert_time, business_attr, phrase, state, tag_scope,esb_seq_no,instucde,instucde_nm,company_nm,company_code,start_user_id,start_user_nm,start_user_org,start_user_endpoint) VALUES (%s, %s, %s, %s, %s, %s, %s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
-        (id,request.app_id, datetime.now(), request.business_attr, request.phrase, TAGGING_STATE.REQUEST.value, tag_scope, request.esb_seq_no,request.instucde,request.instucde_nm,request.company_nm,request.company_code,request.user_id,request.user_nm,request.user_org,request.user_endpoint)
+        """INSERT INTO aitag_tag_log (id,app_id, insert_time, business_attr, phrase, state, tag_scope,esb_seq_no,instucde,instucde_nm,company_nm,company_code,start_user_id,start_user_nm,start_user_org,start_user_endpoint,contract_no) VALUES (%s, %s, %s, %s, %s, %s, %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
+        (id,request.app_id, datetime.now(), request.business_attr, request.phrase, TAGGING_STATE.REQUEST.value, tag_scope, request.esb_seq_no,request.instucde,request.instucde_nm,request.company_nm,request.company_code,request.user_id,request.user_nm,request.user_org,request.user_endpoint,request.contract_no)
     )
     return id
 
-def end_tagging(id:str, result:str):
+def end_tagging(id:str, result:str,x_input:str):
     dao.execute(
-            """UPDATE aitag_tag_log SET state = %s, result = %s, ai_result_endtime = %s WHERE id = %s""",
-            (TAGGING_STATE.END.value, result, datetime.now(), id)
+            """UPDATE aitag_tag_log SET state = %s, result = %s, ai_result_endtime = %s, x_input = %s WHERE id = %s""",
+            (TAGGING_STATE.END.value, result, datetime.now(), x_input, id)
         )
 
 def fail_tagging(id:str):
@@ -134,44 +153,73 @@ def start_tagging(id:str, instucde: Optional[str] = None):
         )
     return is_marine
 
+
+def highlight_long_common_substrings(str_a, str_b):
+    """
+    提取 A 和 B 中长度大于1的共同字符,并将 A 中的这些字符用 <strong> 标记
+    """
+    # 1. 从字符串 B 中提取所有长度大于1的连续字符片段(过滤掉正则符号)
+    # \w+ 会匹配字母、数字和下划线(如果你想匹配中文、字母和数字,可以保留\w;如果只想匹配纯中文,可以改成 [\u4e00-\u9fff]+)
+    b_substrings = re.findall(r'[\w\u4e00-\u9fff]{2,}', str_b)
+    
+    # 2. 去重,并按照长度从长到短排序
+    # 排序非常重要!这能确保先匹配长词(如“海马”),避免短词(如“海马”被拆成“海”和“马”)干扰
+    unique_substrings = sorted(set(b_substrings), key=len, reverse=True)
+    
+    highlighted_a = str_a
+    
+    # 3. 遍历这些长字符串,如果在 A 中出现,就进行高亮替换
+    for substring in unique_substrings:
+        if substring in highlighted_a:
+            # 使用 re.sub 进行替换,re.escape 用于防止字符串中包含特殊正则符号报错
+            highlighted_a = re.sub(f'({re.escape(substring)})', r'<strong>\1</strong>', highlighted_a)
+    
+    return highlighted_a
+
 def generate_html(phrase, matched_rule, tag_name):
-    highlighted_phrase = re.sub(matched_rule, r'<strong>\g<0></strong>', phrase)
+    highlighted_phrase = highlight_long_common_substrings(phrase, matched_rule)
     html_output = f"映射文本【{highlighted_phrase}】与映射规则【{matched_rule}】匹配,映射为标签【{tag_name}】"
     return html_output
 
 # 定义预设规则匹配函数
 def defined_rule_match(phrase: str):
-    phrase = re.sub(r'职业.*?(?=投向)', '', phrase)
+    # phrase = re.sub(r'职业.*?(?=投向)', '', phrase)
+    seen_ids = set()
     result = []
     try:
-        sql = """select DISTINCT on (tag_type,tag_nm) tag_type,tag_nm, defined_rule from aitag_predefined_rules where %s ~ defined_rule and defined_rule is not null and tag_nm is not null order by tag_type,tag_nm,defined_rule desc"""
-        rules = dao.query(sql, (phrase,))
-        print(rules)
+        sql = """select  r.tag_type,r.tag_nm, r.defined_rule from aitag_predefined_rules r
+left join aitag_tag_category c on r.tag_type=c.category_code
+where c.is_delete=0 and c.state = 0  and r.defined_rule is not null and r.tag_nm is not null 
+order by r.tag_type,r.tag_nm,r.defined_rule desc"""
+        rules = dao.query(sql)
         if rules and len(rules) > 0:
             for matched in rules:
-                tag_info = dao.query("""select ati.id,ati.category_id, ati.tag_nm, ati.tag_path,ati.tag_code from aitag_tag_info ati left join aitag_tag_category atc on ati.category_id = atc.id where ati.tag_nm = %s and ati.is_delete = 0 and atc.category_code = %s""", (matched[1], matched[0]))
-                # 安全检查:只有当 tag_info 有数据时才加入结果
-                if tag_info and len(tag_info) > 0:
-                    result.append({
-                        "id": tag_info[0][0],
-                        "desc": generate_html(phrase, matched[2], tag_info[0][2]),
-                        "passr": True,
-                        "tag_code": tag_info[0][4],
-                        "tag_name": tag_info[0][2],
-                        "tag_path": tag_info[0][3],
-                        "category_id": tag_info[0][1]
-                    })
-                else:
-                    logger.warning(f"预设规则匹配成功,但找不到对应的标签记录: tag_nm={matched[1]}, category_code={matched[0]}")
+                try:
+                    if re.search(matched[2], phrase):
+                        tag_info = dao.query("""select ati.id,ati.category_id, ati.tag_nm, ati.tag_path,ati.tag_code from aitag_tag_info ati left join aitag_tag_category atc on ati.category_id = atc.id where ati.tag_nm = %s and ati.is_delete = 0 and atc.category_code = %s""", (matched[1], matched[0]))
+                        # 安全检查:只有当 tag_info 有数据时才加入结果
+                        if tag_info and len(tag_info) > 0 and tag_info[0][0] not in seen_ids:
+                            seen_ids.add(tag_info[0][0])
+                            result.append({
+                                "id": tag_info[0][0],
+                                "desc": generate_html(phrase, matched[2], tag_info[0][2]),
+                                "passr": True,
+                                "tag_code": tag_info[0][4],
+                                "tag_name": tag_info[0][2],
+                                "tag_path": tag_info[0][3],
+                                "category_id": tag_info[0][1]
+                            })
+                except Exception as e:
+                    logger.error(f"Defined rule match failed 1: {e}")
     except Exception as e:
-        logger.error(f"Defined rule match failed: {e}")
+        logger.error(f"Defined rule match failed 2: {e}")
         result = []
     return result
 
 def end_tagging_predefined_rule(id:str, result:str, business_attr: Optional[str] = None):
     dao.execute(
-            """UPDATE aitag_tag_log SET state = %s, result = %s,ai_result_endtime = %s,tagging_channel = %s WHERE id = %s""",
-            (TAGGING_STATE.PREDEFINED_RULE_MATCH.value, result,datetime.now(), TAGGING_CHANNEL.RULES.value, id)
+            """UPDATE aitag_tag_log SET state = %s, result = %s,ai_result_endtime = %s,tagging_channel = %s,feedback=%s WHERE id = %s""",
+            (TAGGING_STATE.PREDEFINED_RULE_MATCH.value, result,datetime.now(), TAGGING_CHANNEL.RULES.value,"agree", id)
         )
     if business_attr is not None and ESB_CALLBACK is not None:
         try:
@@ -190,21 +238,37 @@ def run_ai_pipeline(log_id: str, tag_category_id: str, phrase: str, instucde: Op
             defined_rule_result = defined_rule_match(phrase)    
             if defined_rule_result:
                 logger.info(f"预设规则匹配成功,直接返回结果: {defined_rule_result}")
-                end_tagging_predefined_rule(log_id, json.dumps(defined_rule_result, business_attr))
+                end_tagging_predefined_rule(log_id, json.dumps(defined_rule_result),business_attr)
                 return
 
             # step1: 正则过滤
             result = execute_reg(log_id,tag_category_id,phrase)
+            logger.info(f"正则过滤结果: {result}")
             # step2: 向量检索
-            if not result or len(result) == 0:
-                result = vector_similarity_search(log_id,phrase)
+            # if not result or len(result) == 0 or len(result) >TOP_N: # 正则过滤结果过多或没有结果都进行向量检索,避免正则规则不完善导致的漏匹配问题,同时也避免正则规则过于宽泛导致的过多匹配问题
+            v_result = vector_similarity_search(log_id,phrase)
+            logger.info(f"向量检索结果: {v_result}")
+            # step2.5: 合并结果,取交集优先,交集为空则取并集
+            if  result and len(result) > 0:
+                v_result1 = list(set(result) & set(v_result)) # 取交集,既满足正则规则又满足向量相似度的标签,优先级更高
+                if v_result1 and len(v_result1) > 0:
+                    result = v_result1
+                    logger.info(f"交集结果: {v_result1}")
+                else:
+                    result = list(set(result) | set(v_result)) # 取并集,满足正则规则或者满足向量相似度的标签
+                    if result and len(result) > TOP_N:
+                       result = vector_similarity_search(log_id,phrase,tag_ids=result) # 如果合并后结果过多,则再次进行向量检索过滤一次
+                       logger.info(f"并集后再次向量检索结果: {result}")
+            else:
+                result = v_result
+            logger.info(f"最终候选结果: {result}")
             # step3: LLM 打标
             if result and len(result) > 0:
                 try:
                     tags = dao.query_dict(""" select id,tag_nm as tag_name,tag_code, tag_path,category_id,tag_prompt from aitag_tag_info where id in %s """, (tuple(result),))
                     logger.info(f"筛选结果: {tags}")
                     from agent.agent import reflect_check_sync
-                    result = reflect_check_sync(phrase,is_marine, tags)
+                    result, x_input = reflect_check_sync(phrase,is_marine, tags)
                 except Exception as e:
                     logger.error(f"LLM reflection check failed: {e}")
                     result = None
@@ -212,7 +276,7 @@ def run_ai_pipeline(log_id: str, tag_category_id: str, phrase: str, instucde: Op
                     return
             # step4: 更新数据库
             # 如果result是个空集合,插入None
-            end_tagging(log_id, result if result else None)
+            end_tagging(log_id, result if result else None,x_input)
             
     except Exception as e:
         logger.error(f"[{log_id}] Pipeline failed: {e}")

+ 43 - 13
agent/src/agent/core/es.py

@@ -5,7 +5,7 @@ config = get_config_path()
 
 url = config['es']['url']
 DIMS = int(config['embedding']['default_dims'])
-
+TOP_N = int(config['app'].get("top_n",5))
 
 RRF_CONST:int = 60
 
@@ -84,7 +84,7 @@ def delete_document(doc_id):
 def delete_category_documents(category_id):
     query = {
         "query": {
-            "term": {
+            "match": {
                 "category_id": category_id
             }
         }
@@ -143,26 +143,56 @@ def hybrid_search(query_vector):
     r =  response["hits"]["hits"]
     return [item["_id"] for item in r]
 
-def bm25_vector_search(query:str,query_vector,rrf_score_threshold=0.0157):
-    resp_bm25 = es.search(
-        index=INDEX_NAME,
-        size=10,
-        query={
+def bm25_vector_search(querystr:str,query_vector,tag_ids:list[str]=None,rrf_score_threshold=0.016):
+    
+    query = {
             "multi_match": {
-                "query": query,
-                "fields": ["tag_path", "tag_remark", "tag_prompt"]
+                "query": querystr,
+                "fields": ["tag_path", "tag_remark"]
             }
         }
-    )
-    resp_vector = es.search(
+    if tag_ids and tag_ids != [] and len(tag_ids) > 0:
+        query={
+            "bool": {
+                "must": [
+                    {
+                        "multi_match": {
+                            "query": querystr,
+                            "fields": ["tag_path", "tag_remark"]
+                        }
+                    }
+                ],
+                "filter": [
+                    {
+                        "ids": {
+                            "values": tag_ids  # 确保这里传入的一定是一个非空列表
+                        }
+                    }
+                ]
+            }
+        }
+    
+    resp_bm25 = es.search(
         index=INDEX_NAME,
         size=10,
-        knn={
+        query=query
+    )
+    knn={
             "field": "tag_vector",
             "query_vector": query_vector,
             "k": 10,
             "num_candidates": 100
         }
+    if tag_ids and tag_ids != [] and len(tag_ids) > 0:
+        knn["filter"] = {
+            "ids": {
+                "values": tag_ids  # 传入包含目标 ID 的列表
+            }
+        }
+    resp_vector = es.search(
+        index=INDEX_NAME,
+        size=10,
+        knn=knn
     )
     rrf_scores = {}
     for rank, hit in enumerate(resp_bm25['hits']['hits'], start=1):
@@ -179,7 +209,7 @@ def bm25_vector_search(query:str,query_vector,rrf_score_threshold=0.0157):
     
     sorted_ids = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
     result =  [id for id,score in sorted_ids if score>rrf_score_threshold]
-    return result[:10]
+    return result[:TOP_N]
 
 if __name__ == "__main__":
     results = search_all()

+ 4 - 2
agent/src/agent/main.py

@@ -5,9 +5,10 @@ from agent.logger import logger
 import uvicorn
 from contextlib import asynccontextmanager
 from agent.api_outter import batch_run_async
+from agent.core.config import get_config_path
 
 logger.info("ai-tagging starting!")
-
+breaker = get_config_path()['app'].getboolean("breaker", False)
 api_router = APIRouter()
 api_router.include_router(inner_router,prefix="/admin")
 api_router.include_router(outter_router,prefix="")
@@ -36,7 +37,8 @@ app = FastAPI(
 app.include_router(api_router, prefix="/api/aitag")
 print('API routes initialized')
 def main():
-    print("Starting AI Tagging Service...")
+    logger.info("Starting AI Tagging Service...")
+    logger.info(f"挡板状态: {breaker}")
     uvicorn.run(app, host="0.0.0.0", port=port)
 
 if __name__ == "__main__":

+ 5 - 0
agent/tests/test_config.py

@@ -0,0 +1,5 @@
+from agent.core.config import get_config_path
+
+config = get_config_path()
+breaker = config['app'].getboolean("breaker", False)
+print(breaker)

+ 36 - 0
agent/tests/test_es.py

@@ -0,0 +1,36 @@
+from agent.core.es import es
+from agent.core.vector import get_embeddings
+query = '职业:电动机加工制造,投向:电动机加工制造,用途:电动机加工制造'
+resp_bm25 = es.search(
+        index='ai-tagging',
+        size=10,
+        query={
+            "multi_match": {
+                "query": query,
+                "fields": ["tag_path", "tag_remark", "tag_prompt"]
+            }
+        }
+    )
+for rank, hit in enumerate(resp_bm25['hits']['hits'], start=1):
+    hit["_source"]["tag_vector"] = None
+    doc_id = hit['_id']
+    tag_nm = hit['_source']['tag_name']
+    print(doc_id,tag_nm)
+print('-----------------')
+
+query_vector = get_embeddings([query])[0]
+resp_vector = es.search(
+        index='ai-tagging',
+        size=10,
+        knn={
+            "field": "tag_vector",
+            "query_vector": query_vector,
+            "k": 10,
+            "num_candidates": 100
+        }
+    )
+for rank, hit in enumerate(resp_vector['hits']['hits'], start=1):
+    hit["_source"]["tag_vector"] = None
+    doc_id = hit['_id']
+    tag_nm = hit['_source']['tag_name']
+    print(doc_id,tag_nm)

+ 32 - 29
agent/tests/test_reg.py

@@ -1,31 +1,34 @@
 import re
 
-regstr = """
-^(?!.*(贷款|借款|融资|采购|销售|建设|工程|制造|生产|养殖|捕捞|贸易|物流|运输|旅游|酒店|餐饮|房地产|个人消费)).*(海洋.{0,100}(协会|学会|商会|联盟|联合会|促进会|研究会|俱乐部|公益组织|基金会|社会组织|非营利组织|NGO)|(协会|学会|商会|联盟|联合会|促进会|研究会|俱乐部|公益组织|基金会|社会组织|非营利组织|NGO).{0,100}海洋)
-"""
-
-test_cases = [
-    "防腐材料",       
-    "海洋防污涂层",     
-    "船舶防腐",        
-    "职业:水产养殖人员 投向:内陆养殖 用途:养殖鲍鱼", 
-    "材料",           
-    "医疗防护服",
-    "职业:艺术从业者;投向:环保投资;用途:海洋垃圾清理公益组织"      
-]
-
-pattern = re.compile(regstr, re.VERBOSE)
-
-for t in test_cases:
-    print(f"{t!r}: {'✓' if pattern.match(t) else '✗'}")
-
-a = False
-b = False
-print(a == b)
-
-
-text = "职业:水产养殖人员 投向:内陆养殖 用途:其他海洋服务,老年人"
-# 匹配从“职业”开始,直到“投向”之前的所有内容,并将其替换为空字符串
-result = re.sub(r'职业:.*?(?=投向)', '', text)
-
-print(result)
+def highlight_long_common_substrings(str_a, str_b):
+    """
+    提取 A 和 B 中长度大于1的共同字符,并将 A 中的这些字符用 <strong> 标记
+    """
+    # 1. 从字符串 B 中提取所有长度大于1的连续字符片段(过滤掉正则符号)
+    # \w+ 会匹配字母、数字和下划线(如果你想匹配中文、字母和数字,可以保留\w;如果只想匹配纯中文,可以改成 [\u4e00-\u9fff]+)
+    b_substrings = re.findall(r'[\w\u4e00-\u9fff]{2,}', str_b)
+    
+    # 2. 去重,并按照长度从长到短排序
+    # 排序非常重要!这能确保先匹配长词(如“海马”),避免短词(如“海马”被拆成“海”和“马”)干扰
+    unique_substrings = sorted(set(b_substrings), key=len, reverse=True)
+    
+    highlighted_a = str_a
+    
+    # 3. 遍历这些长字符串,如果在 A 中出现,就进行高亮替换
+    for substring in unique_substrings:
+        if substring in highlighted_a:
+            # 使用 re.sub 进行替换,re.escape 用于防止字符串中包含特殊正则符号报错
+            highlighted_a = re.sub(f'({re.escape(substring)})', r'<strong>\1</strong>', highlighted_a)
+    
+    return highlighted_a
+
+def generate_html(phrase, matched_rule, tag_name):
+    highlighted_phrase = highlight_long_common_substrings(phrase, matched_rule)
+    html_output = f"映射文本【{highlighted_phrase}】与映射规则【{matched_rule}】匹配,映射为标签【{tag_name}】"
+    return html_output
+
+phrase = "投向:环境保护专用设备制造; 用途:养海马"
+matched_rule = "环境保护专用设备制造|环境监测专用仪器仪表制造"
+tag_name = "环保专用设备仪器制造业"
+
+print(generate_html(phrase, matched_rule, tag_name))

+ 6 - 0
agent/tests/test_reg1.py

@@ -0,0 +1,6 @@
+import re
+
+
+print(re.search('海洋天然气及可燃冰开采1', """职业:海洋天然气及可燃冰开采,
+                投向:海洋天然气及可燃冰开采1,
+                用途:海洋天然气及可燃冰开采"""))

+ 145 - 0
agent/tests/test_rerank.py

@@ -0,0 +1,145 @@
+import requests
+import json
+system_prompt = """ 依据Query对Tag进行关联性排序,每个Tag包含id、tag_nm(标签名称)、remark(标签描述)三个字段,输出排序后的Tag列表,关联性越高,排序越靠前,输出tag信息中只包含id即可 """
+query = """职业:电动机加工制造,投向:电动机加工制造,用途:电动机加工制造"""
+# 20260507002 中药饮片加工
+# 20260200007 海洋鱼糜制品及水产品干腌制加工
+# 20260507025 工业控制计算机及系统制造
+# 20260200079 海洋化工产品制造
+# 20260507013 电子工业专用设备制造
+# 20260200078 海洋水产品深加工
+# 20260200063 海洋渔业和水产品加工设备制造
+# 20260507014 光纤、光缆及锂离子电池制造
+# 20260507022 计算机整机制造
+# 20260507106 电子专用设备制造
+# 20260507117 电气设备制造
+# 20260507022 计算机整机制造
+# 20260507114 专用设备制造
+# 20260507113 通用设备制造
+# 20260507116 汽车与轨道设备制造
+# 20260507013 电子工业专用设备制造
+# 20260507106 电子专用设备制造
+# 20260507119 其他装备制造
+# 20260507016 广播电视设备制造
+# 20260507102 计算机制造
+tag_infos = [
+    {
+        "id": "20260507002",
+        "tag_nm":"中药饮片加工",
+        "remark":"中药饮片加工"
+    },
+    {
+        "id": "20260200007",
+        "tag_nm":"海洋鱼浆制品及水产品干腌制加工",
+        "remark":"海洋鱼浆制品及水产品干腌制加工"
+    },
+    {
+        "id": "20260507025",
+        "tag_nm":"工业控制计算机及系统制造",
+        "remark":"工业控制计算机及系统制造"
+    },
+    {
+        "id": "20260200079",
+        "tag_nm":"海洋化工产品制造",
+        "remark":"海洋化工产品制造"
+    },
+    {
+        "id": "20260507013",
+        "tag_nm":"电子工业专用设备制造",
+        "remark":"电子工业专用设备制造"
+    },
+    {
+        "id": "20260200078",
+        "tag_nm":"海洋水产品加工",
+        "remark":"海洋水产品加工"
+    },
+    {
+        "id": "20260200063",
+        "tag_nm":"海洋渔业和水产品加工设备制造",
+        "remark":"海洋渔业和水产品加工设备制造"
+    },
+    {
+        "id": "20260507014",
+        "tag_nm":"光纤、光缆及锂离子电池制造",
+        "remark":"光纤、光缆及锂离子电池制造"
+    },
+    {
+        "id": "20260507022",
+        "tag_nm":"计算机整机制造",
+        "remark":"计算机整机制造"
+    },
+    {
+        "id": "20260507106",
+        "tag_nm":"电子专用设备制造",
+        "remark":"电子专用设备制造"
+    },
+    {
+        "id": "20260507117",
+        "tag_nm":"电气设备制造",
+        "remark":"电子专用设备制造"
+    },
+    {
+        "id": "20260507022",
+        "tag_nm":"计算机整机制造",
+        "remark":"计算机整机制造"
+    },
+    {
+        "id": "20260507114",
+        "tag_nm":"专用设备制造",
+        "remark":"专用设备制造"
+    },
+    {
+        "id": "20260507113",
+        "tag_nm":"通用设备制造",
+        "remark":"通用设备制造"
+    },
+    {
+        "id": "20260507116",
+        "tag_nm":"汽车与轨道设备制造",
+        "remark":"汽车与轨道设备制造"
+    },
+    {
+        "id": "20260507013",
+        "tag_nm":"电子工业专用设备制造",
+        "remark":"电子工业专用设备制造"
+    },
+    {
+        "id": "20260507106",
+        "tag_nm":"电子专用设备制造",
+        "remark":"电子专用设备制造"
+    },
+    {
+        "id": "20260507119",
+        "tag_nm":"其他装备制造",
+        "remark":"其他装备制造"
+    },
+    {
+        "id": "20260507016",
+        "tag_nm":"广播电视设备制造",
+        "remark":"广播电视设备制造"
+    },
+    {
+        "id": "20260507102",
+        "tag_nm":"计算机制造",
+        "remark":"计算机制造"
+    }
+]
+    
+tags = [f"<Tag>:{json.dumps(tag,ensure_ascii=False)}>" for tag in tag_infos]
+response = requests.post(
+    "http://10.192.72.12:18088/rerank",
+    json={
+        "model": "qwen3-reranker-4b-awq-int4",
+        "query": """
+        <|im_start|>system
+        {system_prompt}
+        <|im_end|>
+        <|im_start|>user
+        <Query>: {query}
+        <|im_end|>
+        """,
+        "documents": tags,
+        "top_n": 5
+    }
+)
+print(response.json()["results"])

+ 8 - 0
agent/tests/test_similar.py

@@ -0,0 +1,8 @@
+from agent.core.es import bm25_vector_search
+from agent.core.vector import get_embeddings    
+
+phrase = "职业:海鲜批发;投向:海鲜批发;用途:海鲜批发"
+phrase = "海鲜批发"
+embeddings = get_embeddings([phrase])[0]
+
+print(bm25_vector_search(phrase, embeddings,rrf_score_threshold=0.015))

+ 3 - 0
agent/tests/test_split.py

@@ -0,0 +1,3 @@
+str = "abc@|@456@|@7890"
+split_str = str.split("@|@")
+print(split_str)

+ 3 - 3
agent/tests/test_sync_category.py

@@ -3,13 +3,13 @@ import requests
 # res = requests.post("http://10.192.72.13:9876/api/aitag/admin/v1/synchronize_category", json={
 #     "category_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
 # })
-res = requests.post("http://localhost:9876/api/aitag/admin/v1/synchronize_category", json={
+res = requests.post("http://10.192.72.13:9876/api/aitag/admin/v1/synchronize_category", json={
     "category_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
 })
-res = requests.post("http://localhost:9876/api/aitag/admin/v1/synchronize_category", json={
+res = requests.post("http://10.192.72.13:9876/api/aitag/admin/v1/synchronize_category", json={
     "category_id": "0a2dc889-6205-4cb2-be31-d67c6390a0d6"
 })
-res = requests.post("http://localhost:9876/api/aitag/admin/v1/synchronize_category", json={
+res = requests.post("http://10.192.72.13:9876/api/aitag/admin/v1/synchronize_category", json={
     "category_id": "cd4de5d4-491f-4779-8d96-9246c861e907"
 })
 print(res.text)

+ 13 - 7
agent/tests/test_tagging.py

@@ -3,14 +3,21 @@ import logging
 logging.basicConfig(level=logging.INFO, force=True,format='%(asctime)s - %(levelname)s - %(message)s')
 logging.info("app starting!")
 
-for i in range(10):
+querys =[ '职业:电动机加工制造,投向:电动机加工制造,用途:电动机加工制造',
+ '职业:通信系统设备研发制造,投向:通信系统设备研发制造,用途:通信系统设备研发制造',
+ '职业:疗养院,用途:无还本续贷,经营范围:疗养院',
+ '投向:远洋货物运输,用途:经营运输车队,经营范围:',
+ '职业:舒缓医疗服务,投向:舒缓医疗服务,用途:舒缓医疗服务',
+ '职业:安宁照护服务,投向:安宁照护服务,用途:安宁照护服务',
+ '主营范围:大都市方法 贷款投向:的萨芬撒飞洒 贷款用途:海洋服务']
+
+for i in range(1):
+    # query = querys[i % len(querys)]
+    query = querys[6]
     res = requests.post("http://10.192.72.13:9876/api/aitag/v1/tagging", json={
-        # "app_id": "test_app",
-        # "timestamp": 1234567890,
-        # "sign": "test_sign",
         "esb_seq_no":"abc",
-        "business_attr": "test_attr14"+str(i),
-        "phrase": "养点生蚝卖",
+        "business_attr": "test_attr64",
+        "phrase": query,
         "instucde": "901020300",
         "contract_no":"contract_no",
         "instucde_nm":"instucde_nm",
@@ -23,4 +30,3 @@ for i in range(10):
     })
     logging.info(f"i:{i}, res:{res.text}")
 
-

+ 0 - 1
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/controller/AitagTagLogController.java

@@ -55,7 +55,6 @@ public class AitagTagLogController {
 
     /**
      * 我的配置
-     *
      * @param taggingResult 智能打标结果
      * @return ResultDto
      */

+ 9 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/dto/TagLogDto.java

@@ -172,4 +172,13 @@ public class TagLogDto {
      **/
     @ApiModelProperty(value = "标签标识 0:正常数据; 1:测试数据")
     private String tagScope;
+
+    @ApiModelProperty(value="发起人ID")
+    private String startUserId;
+
+    @ApiModelProperty(value="发起人机构")
+    private String startUserOrg;
+
+    @ApiModelProperty(value="打标渠道")
+    private String taggingChannel;
 }

+ 3 - 2
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/entity/AitagTagLogEntity.java

@@ -133,8 +133,6 @@ passr: true/false
     @ApiModelProperty(value = "合同编号")
     private String contractNo;
 
-
-
     /**
      * 正则过滤结果
      **/
@@ -170,4 +168,7 @@ passr: true/false
 
     @ApiModelProperty(value="发起人机构")
     private String startUserOrg;
+
+    @ApiModelProperty(value="打标渠道")
+    private String taggingChannel;
 }

+ 1 - 1
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/vo/SmartTaggingResultVo.java

@@ -41,7 +41,7 @@ public class SmartTaggingResultVo extends PageQuery {
 
 
     public String getCategoryExp() {
-        return "[{\"category_id\": \""+categoryId+"\"}]";
+        return "[{\"category_id\": \""+categoryId+"\"},{\"passr\": true}]";
     }
 
     public String getStartTaggingTime() {

+ 3 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/vo/fastapivo/AiTaggingQueryResponseVo.java

@@ -87,6 +87,9 @@ public class AiTaggingQueryResponseVo {
         @ApiModelProperty(value = "分类 ID")
         private String category_id;
 
+        @ApiModelProperty(value="打标渠道")
+        private Integer tagging_channel;
+
         public String getInsert_time() {
             return formatDateTime(insert_time);
         }

+ 60 - 12
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/impl/AitagTagLogServiceImpl.java

@@ -13,6 +13,7 @@ import cn.com.yusys.yusp.util.Roles;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -55,17 +56,43 @@ public class AitagTagLogServiceImpl extends ServiceImpl<AitagTagLogDao, AitagTag
     private static final DateTimeFormatter DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
     private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
 
-    @Override
-    public DataOverviewVo dataOverview(SmartTaggingResultVo taggingResult) {
-        DataOverviewVo dataOverview = new DataOverviewVo();
+    private LambdaQueryWrapper<AitagTagLogEntity> build(SmartTaggingResultVo taggingResult){
+        LambdaQueryWrapper<AitagTagLogEntity> queryWrapper = new LambdaQueryWrapper<>();
+        if (StringUtils.isNotBlank(taggingResult.getCategoryId())) {
+            queryWrapper.apply("result @> '"+taggingResult.getCategoryExp()+"'");
+        }
+        if (StringUtils.isNotBlank(taggingResult.getStartTaggingTime())){
+            queryWrapper.ge(AitagTagLogEntity::getInsertTime, taggingResult.getStartTaggingTime());
+        }
+        if (StringUtils.isNotBlank(taggingResult.getEndTaggingTime())){
+            queryWrapper.le(AitagTagLogEntity::getInsertTime, taggingResult.getEndTaggingTime());
+        }
+//        queryWrapper.in(AitagTagLogEntity::getState, DataDictionary.RESULT_PUSHED,DataDictionary.MANAGER_CONFIRMED,MATCHED_RULES);
+        queryWrapper.notIn(AitagTagLogEntity::getState,0,5);
+        queryWrapper.eq(AitagTagLogEntity::getTagScope, PROD_DATA);
+        queryWrapper.eq(AitagTagLogEntity::getIsDelete, NON_DELETE);
 
-        Integer totalCnt = this.baseMapper.selectTagCount(taggingResult);
-        log.info("查询出符合条件的全部打标数据:{} 条",totalCnt);
-        dataOverview.setCountNum(totalCnt);
+        if(taggingResult.getUserId()!=null && taggingResult.getRoleCode()!=null && taggingResult.getRoleCode().equals(Roles.TAG_MANAGER)){
+            queryWrapper.and(w -> w
+                    .eq(AitagTagLogEntity::getStartUserId, taggingResult.getUserId())
+                    .or()
+                    .eq(AitagTagLogEntity::getFeedbackUserId, taggingResult.getUserId())
+            );
+        }
+        if(taggingResult.getUserId()!=null && taggingResult.getRoleCode()!=null && taggingResult.getRoleCode().equals(Roles.TAG_ADMIN)){
+            queryWrapper.and(w -> w
+                    .in(AitagTagLogEntity::getStartUserOrg, taggingResult.getOrgs())
+                    .or()
+                    .in(AitagTagLogEntity::getStartUserOrg, taggingResult.getOrgs())
+            );
+        }
+        return queryWrapper;
+    }
 
+    private LambdaQueryWrapper<AitagTagLogEntity> build1(SmartTaggingResultVo taggingResult){
         LambdaQueryWrapper<AitagTagLogEntity> queryWrapper = new LambdaQueryWrapper<>();
         if (StringUtils.isNotBlank(taggingResult.getCategoryId())) {
-            queryWrapper.apply("result @> '"+taggingResult.getCategoryExp()+"'");
+            queryWrapper.apply("feedback_result @> '"+taggingResult.getCategoryExp()+"'");
         }
         if (StringUtils.isNotBlank(taggingResult.getStartTaggingTime())){
             queryWrapper.ge(AitagTagLogEntity::getInsertTime, taggingResult.getStartTaggingTime());
@@ -73,7 +100,8 @@ public class AitagTagLogServiceImpl extends ServiceImpl<AitagTagLogDao, AitagTag
         if (StringUtils.isNotBlank(taggingResult.getEndTaggingTime())){
             queryWrapper.le(AitagTagLogEntity::getInsertTime, taggingResult.getEndTaggingTime());
         }
-        queryWrapper.in(AitagTagLogEntity::getState, DataDictionary.RESULT_PUSHED,DataDictionary.MANAGER_CONFIRMED,MATCHED_RULES);
+//        queryWrapper.in(AitagTagLogEntity::getState, DataDictionary.RESULT_PUSHED,DataDictionary.MANAGER_CONFIRMED,MATCHED_RULES);
+        queryWrapper.notIn(AitagTagLogEntity::getState,0,5);
         queryWrapper.eq(AitagTagLogEntity::getTagScope, PROD_DATA);
         queryWrapper.eq(AitagTagLogEntity::getIsDelete, NON_DELETE);
 
@@ -91,12 +119,24 @@ public class AitagTagLogServiceImpl extends ServiceImpl<AitagTagLogDao, AitagTag
                     .in(AitagTagLogEntity::getStartUserOrg, taggingResult.getOrgs())
             );
         }
+        return queryWrapper;
+    }
+
+    @Override
+    public DataOverviewVo dataOverview(SmartTaggingResultVo taggingResult) {
+        DataOverviewVo dataOverview = new DataOverviewVo();
 
+        Integer totalCnt = this.baseMapper.selectTagCount(taggingResult);
+        log.info("查询出符合条件的全部打标数据:{} 条",totalCnt);
+        dataOverview.setCountNum(totalCnt);
+
+        LambdaQueryWrapper<AitagTagLogEntity> queryWrapper = build(taggingResult);
         Integer totalConfirmedCnt = this.baseMapper.selectCount(queryWrapper);
         log.info("查询出符合条件的打标完成数据:{} 条",totalConfirmedCnt);
 
-        queryWrapper.eq(AitagTagLogEntity::getFeedback,FEEDBACK_RESULT_AGREE);
-        Integer agreeTagNum = this.baseMapper.selectCount(queryWrapper);
+        LambdaQueryWrapper<AitagTagLogEntity> queryWrapper1 = build(taggingResult);
+        queryWrapper1.eq(AitagTagLogEntity::getFeedback,FEEDBACK_RESULT_AGREE);
+        Integer agreeTagNum = this.baseMapper.selectCount(queryWrapper1);
         log.info("查询出符合条件的打标准确数据:{} 条",agreeTagNum);
         dataOverview.setAccurateNum(agreeTagNum);
 
@@ -111,8 +151,16 @@ public class AitagTagLogServiceImpl extends ServiceImpl<AitagTagLogDao, AitagTag
             dataOverview.setManualAdjustRate(BigDecimal.valueOf(100).subtract(accurateRate)
                     .setScale(2, RoundingMode.HALF_UP)+"%");
         }
-        dataOverview.setManualAdjustCount(totalConfirmedCnt-agreeTagNum);
-        dataOverview.setTaggedUnconfirmed(totalCnt-totalConfirmedCnt);
+
+        LambdaQueryWrapper<AitagTagLogEntity> queryWrapper2 = build1(taggingResult);
+        queryWrapper2.eq(AitagTagLogEntity::getFeedback,FEEDBACK_RESULT_REJECT);
+        Integer manualAdjustCount = this.baseMapper.selectCount(queryWrapper2);
+        dataOverview.setManualAdjustCount(manualAdjustCount);
+
+        LambdaQueryWrapper<AitagTagLogEntity> queryWrapper3 = build(taggingResult);
+        queryWrapper3.eq(AitagTagLogEntity::getState,1);
+        Integer unconfirmed = this.baseMapper.selectCount(queryWrapper3);
+        dataOverview.setTaggedUnconfirmed(unconfirmed);
         log.info("准备返回数据为:{} ", JSONObject.toJSONString(dataOverview));
         return dataOverview;
     }

+ 6 - 2
server/yusp-tagging-core/src/main/resources/mapper/AitagTagLogMapper.xml

@@ -20,6 +20,10 @@
         <result column="reg_result" jdbcType="VARCHAR" property="regResult"/>
         <result column="feedback_user_org" jdbcType="VARCHAR" property="feedbackUserOrg"/>
         <result column="feedback_user_endpoint" jdbcType="VARCHAR" property="feedbackUserEndpoint"/>
+        <result column="is_delete" jdbcType="INTEGER" property="isDelete"/>
+        <result column="start_user_id" jdbcType="VARCHAR" property="startUserId"/>
+        <result column="start_user_org" jdbcType="VARCHAR" property="startUserOrg"/>
+        <result column="tagging_channel" jdbcType="VARCHAR" property="taggingChannel"/>
     </resultMap>
 
 
@@ -36,8 +40,8 @@
             <if test="endTaggingTime != null and endTaggingTime != ''">
                 AND #{endTaggingTime} >= insert_time
             </if>
-            <if test="categoryId != null and categoryId != ''">
-                AND result @> #{categoryExp}::jsonb
+            <if test="categoryId != null and categoryId != '' ">
+                AND (result @> #{categoryExp}::jsonb or feedback_result @> #{categoryExp}::jsonb)
             </if>
             <if test="roleCode != null and roleCode == 'aitag02' and userId != null">
                 AND (start_user_id = #{userId} or feedback_user_id = #{userId})

+ 14 - 0
server/yusp-tagging-starter/src/test/java/cn/com/yusys/yusp/TestManagerAuth.java

@@ -1,19 +1,25 @@
 package cn.com.yusys.yusp;
 
 import cn.com.yusys.yusp.detail.App;
+import cn.com.yusys.yusp.domain.dto.TagLogDto;
 import cn.com.yusys.yusp.feign.OcaOrgListRequest;
 import cn.com.yusys.yusp.feign.OcaOrgListResponse;
 import cn.com.yusys.yusp.feign.OcaUserService;
+import cn.com.yusys.yusp.service.AitagTagLogService;
 import com.alibaba.fastjson.JSON;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 
+import javax.annotation.Resource;
+
 @SpringBootTest(classes = App.class)
 public class TestManagerAuth {
 
     @Autowired
     private OcaUserService ocaUserService;
+    @Resource
+    private AitagTagLogService aitagTagLogService;
 
     @Test
     public void test2() {
@@ -22,4 +28,12 @@ public class TestManagerAuth {
         OcaOrgListResponse response =  ocaUserService.sysorglist(request);
         System.out.println(JSON.toJSONString(response));
     }
+
+    @Test
+    public void test3() {
+        TagLogDto taggingDetailsResDTO = aitagTagLogService.show("471adf73df02409ab7288ac0235eac49");
+        System.out.println(JSON.toJSONString(taggingDetailsResDTO));
+        System.out.println(taggingDetailsResDTO.getStartUserId());
+        System.out.println(taggingDetailsResDTO.getTaggingChannel());
+    }
 }