Procházet zdrojové kódy

Restore local DMS schema and handle verified empty lists

wangxi před 1 měsícem
rodič
revize
3c4f8e1b17

+ 34 - 34
config/dms_schema.json

@@ -2,23 +2,23 @@
   "parent": "营商助手",
   "columns": {
     "records": {
-      "id": 3049,
+      "id": 1889,
       "name": "助手问答记录",
       "fields": {
-        "c_session_id": "c_c_session_id", "c_question": "c_c_question", "c_answer": "c_c_answer",
-        "c_created_at": "c_c_created_at"
+        "c_session_id": "c_session_id", "c_question": "c_question", "c_answer": "c_answer",
+        "c_created_at": "c_created_at"
       },
       "types": {"c_created_at": "timestamp"}
     },
     "sessions": {
-      "id": 3055,
+      "id": 1887,
       "name": "助手会话",
       "fields": {
-        "c_session_id": "c_c_session_id", "qpyszx": "c_qpyszx", "qyzt": "c_qyzt", "rzqpyx": "c_rzqpyx",
-        "c_focus_points": "c_c_focus_points", "c_settlement_intent_level": "c_c_settlement_intent_level",
-        "c_core_needs": "c_c_core_needs", "jygjbm": "c_jygjbm", "summary": "c_summary",
-        "c_description": "c_c_description", "c_title": "c_c_title",
-        "c_created_at": "c_c_created_at", "c_updated_at": "c_c_updated_at"
+        "c_session_id": "c_session_id", "qpyszx": "qpyszx", "qyzt": "qyzt", "rzqpyx": "rzqpyx",
+        "c_focus_points": "c_focus_points", "c_settlement_intent_level": "c_settlement_intent_level",
+        "c_core_needs": "c_core_needs", "jygjbm": "jygjbm", "summary": "summary",
+        "c_description": "c_description", "c_title": "c_title",
+        "c_created_at": "c_created_at", "c_updated_at": "c_updated_at"
       },
       "types": {
         "qpyszx": "boolean", "qyzt": "boolean", "rzqpyx": "boolean",
@@ -26,42 +26,42 @@
       }
     },
     "demands": {
-      "id": 3043,
+      "id": 1891,
       "name": "诉求词",
-      "fields": {"c_session_id": "c_c_session_id", "c_word": "c_c_word", "c_created_at": "c_c_created_at"},
+      "fields": {"c_session_id": "c_session_id", "c_word": "c_word", "c_created_at": "c_created_at"},
       "types": {"c_created_at": "timestamp"}
     },
     "basic": {
-      "id": 3037,
+      "id": 1888,
       "name": "企业基础信息",
       "fields": {
-        "c_name": "c_c_name", "c_no": "c_c_no", "c_belong_org": "c_c_belong_org",
-        "c_oper_id": "c_c_oper_id", "c_oper_name": "c_c_oper_name", "c_start_date": "c_c_start_date",
-        "c_end_date": "c_c_end_date", "c_status": "c_c_status", "c_province": "c_c_province",
-        "c_updated_date": "c_c_updated_date", "c_credit_code": "c_c_credit_code",
-        "c_regist_capi": "c_c_regist_capi", "c_registered_capital": "c_c_registered_capital",
-        "c_registered_capital_unit": "c_c_registered_capital_unit",
-        "c_registered_capital_ccy": "c_c_registered_capital_ccy", "c_econ_kind": "c_c_econ_kind",
-        "c_address": "c_c_address", "c_scope": "c_c_scope", "c_term_start": "c_c_term_start",
-        "c_term_end": "c_c_term_end", "c_check_date": "c_c_check_date", "c_org_no": "c_c_org_no",
-        "c_is_on_stock": "c_c_is_on_stock", "c_stock_number": "c_c_stock_number",
-        "c_stock_type": "c_c_stock_type", "c_image_url": "c_c_image_url", "c_ent_type": "c_c_ent_type",
-        "c_rec_cap": "c_c_rec_cap", "c_paid_up_capital": "c_c_paid_up_capital",
-        "c_paid_up_capital_unit": "c_c_paid_up_capital_unit",
-        "c_paid_up_capital_ccy": "c_c_paid_up_capital_ccy", "c_area_code": "c_c_area_code",
-        "c_area": "c_c_area", "c_revoke_info": "c_c_revoke_info",
-        "c_original_name": "c_c_original_name",
-        "c_designated_representative_list": "c_c_designated_representative_list"
+        "c_name": "c_name", "c_no": "c_no", "c_belong_org": "c_belong_org",
+        "c_oper_id": "c_oper_id", "c_oper_name": "c_oper_name", "c_start_date": "c_start_date",
+        "c_end_date": "c_end_date", "c_status": "c_status", "c_province": "c_province",
+        "c_updated_date": "c_updated_date", "c_credit_code": "c_credit_code",
+        "c_regist_capi": "c_regist_capi", "c_registered_capital": "c_registered_capital",
+        "c_registered_capital_unit": "c_registered_capital_unit",
+        "c_registered_capital_ccy": "c_registered_capital_ccy", "c_econ_kind": "c_econ_kind",
+        "c_address": "c_address", "c_scope": "c_scope", "c_term_start": "c_term_start",
+        "c_term_end": "c_term_end", "c_check_date": "c_check_date", "c_org_no": "c_org_no",
+        "c_is_on_stock": "c_is_on_stock", "c_stock_number": "c_stock_number",
+        "c_stock_type": "c_stock_type", "c_image_url": "c_image_url", "c_ent_type": "c_ent_type",
+        "c_rec_cap": "c_rec_cap", "c_paid_up_capital": "c_paid_up_capital",
+        "c_paid_up_capital_unit": "c_paid_up_capital_unit",
+        "c_paid_up_capital_ccy": "c_paid_up_capital_ccy", "c_area_code": "c_area_code",
+        "c_area": "c_area", "c_revoke_info": "c_revoke_info",
+        "c_original_name": "c_original_name",
+        "c_designated_representative_list": "c_designated_representative_list"
       }
     },
     "honor": {
-      "id": 3050,
+      "id": 1886,
       "name": "企业荣誉信息",
       "fields": {
-        "c_credit_code": "c_c_credit_code", "c_name": "c_c_name", "c_level": "c_c_level",
-        "c_source": "c_c_source", "c_publish_office": "c_c_publish_office",
-        "c_publish_date": "c_c_publish_date", "c_beging_date": "c_c_beging_date",
-        "c_dead_line": "c_c_dead_line", "c_certificate_code": "c_c_certificate_code"
+        "c_credit_code": "c_credit_code", "c_name": "c_name", "c_level": "c_level",
+        "c_source": "c_source", "c_publish_office": "c_publish_office",
+        "c_publish_date": "c_publish_date", "c_beging_date": "c_beging_date",
+        "c_dead_line": "c_dead_line", "c_certificate_code": "c_certificate_code"
       }
     }
   }

+ 5 - 1
docs/dms-schema.md

@@ -8,4 +8,8 @@
 
 程序仍先按栏目 ID 读取元数据,核对栏目名、父栏目和模型字段类型,再访问内容。字段名映射应用于查询条件、写入内容和读取结果;配置非法或服务端模型不符会停止相应 DMS 操作。`sh deploy-offline.sh check-dms`也读取本文件及现有环境变量,按实际映射只读核对五个栏目的元数据。修改配置不能替代服务端栏目、模型和历史数据迁移。离线部署须重新生成单包,旧包不会自动包含新配置。
 
-新环境可在`.env`中设置`DMS_BASE_URL`为服务主机、`AUTH_PORT`为认证端口、`DMS_PORT`为数据端口。客户端向认证端口的`/proxy_oauth/user/login`获取token,再向数据端口的`/proxy_dms`访问栏目与模型;未设置端口时沿用`DMS_BASE_URL`的端口。`check-dms`只验证栏目元数据和字段类型,不因栏目暂无记录而失败。当前配置中3043为诉求词、3037为企业基础信息;其他栏目保留原ID。
+新环境可在`.env`中设置`DMS_BASE_URL`为服务主机、`AUTH_PORT`为认证端口、`DMS_PORT`为数据端口。客户端使用账号密码向认证端口的`/proxy_oauth/user/login`获取token,token仅保存在内存,再向数据端口的`/proxy_dms`访问栏目与模型;未设置端口时沿用`DMS_BASE_URL`的端口。HTTP 401或DMS 212时有界重新登录;无需手动获取token。直接`/dms`且未配置认证端口时仍要求提供token,避免猜测认证地址。`check-dms`只验证栏目元数据和字段类型,不因栏目暂无记录而失败。
+
+2026-09-30的`dev`本地配置已依据当前服务核验:父栏目“营商助手”1883,助手问答记录1889、助手会话1887、诉求词1891、企业基础信息1888、企业荣誉信息1886。五个名称均与真实元数据一致;65个字段映射value各删除一层`c_`并通过模型类型核验。其他环境应重新核验,不能直接复用这些ID。
+
+会话与企业列表共用精确空结果契约:仅`selectContentList`返回`code=202`且`message`与`content`均为“数据不存在”时视为空列表。其他202、权限/认证失败、元数据和写入错误仍明确失败。真实合成草稿已验证新增、更新及回读;销毁请求`state=4`获成功响应,草稿列表确认测试记录退出。当前服务未返回销毁状态的记录,因此尚未验证销毁记录的归档回读或可恢复性;不能把销毁操作称为物理删除。

+ 0 - 8
src/step3_question_answer/dms_company.py

@@ -116,14 +116,6 @@ class DmsCompanyClient(DmsClient):
     def honor_id(self):
         return schema_column(self.config.schema, 'honor')['id']
 
-    def normalize_envelope(self, url, envelope):
-        # Verified company-list empty contract; never normalize auth, unknown
-        # failures, metadata calls or the session writer's responses.
-        if (url == self.api + '/content/selectContentList' and envelope.get('code') == 202
-                and envelope.get('message') == '数据不存在' and envelope.get('content') == '数据不存在'):
-            return {'code': 200, 'content': {'data': [], 'count': 0}}
-        return envelope
-
     def call(self, path, form):
         if path not in ('/column/getColumnById', '/model/getModelById', '/content/selectContentList'):
             raise DmsError('dms_operation_denied')

+ 5 - 0
src/step5_session_analysis/dms.py

@@ -181,6 +181,11 @@ class DmsClient:
         return envelope
 
     def normalize_envelope(self, url, envelope):
+        # Verified local DMS list contract: this exact envelope means no rows.
+        # Never normalize writes, metadata, permission errors or other failures.
+        if (url == self.api + '/content/selectContentList' and envelope.get('code') == 202
+                and envelope.get('message') == '数据不存在' and envelope.get('content') == '数据不存在'):
+            return {'code': 200, 'content': {'data': [], 'count': 0}}
         return envelope
 
     def call(self, path, form):

+ 24 - 0
src/step5_session_analysis/tests/test_dms.py

@@ -11,6 +11,30 @@ from src.step5_session_analysis.dms import (
 )
 
 
+class EmptyListContractTests(unittest.TestCase):
+    def test_session_empty_list_is_not_a_service_failure(self):
+        config = DmsConfig('https://example.invalid', token='synthetic')
+        envelope = {'code': 202, 'message': '数据不存在', 'content': '数据不存在'}
+        client = DmsClient(config, lambda *args: envelope)
+        client.columns[config.sessions_id] = {}
+        self.assertEqual(client.rows(config.sessions_id, 'synthetic-missing'), [])
+
+    def test_empty_contract_does_not_hide_other_failures_or_writes(self):
+        config = DmsConfig('https://example.invalid', token='synthetic')
+        empty = {'code': 202, 'message': '数据不存在', 'content': '数据不存在'}
+        for path, form, envelope in (
+            ('/content/addContent', {'columnId': config.sessions_id, 'content': '{}'}, empty),
+            ('/content/updateContent', {'columnId': config.sessions_id, 'content': '{}'}, empty),
+            ('/column/getColumnById', {'id': config.sessions_id}, empty),
+            ('/content/selectContentList', {'columnId': config.sessions_id}, {**empty, 'message': 'unknown'}),
+            ('/content/selectContentList', {'columnId': config.sessions_id}, {**empty, 'content': None}),
+        ):
+            with self.subTest(path=path, envelope=envelope):
+                client = DmsClient(config, lambda *args: envelope)
+                with self.assertRaisesRegex(DmsError, 'dms_service_failed'):
+                    client.call(path, form)
+
+
 class Transport:
     def __init__(self):
         self.calls = []