diff --git a/app/server/somni/quiz/rpc.py b/app/server/somni/quiz/rpc.py index 593d1ed..713c77f 100644 --- a/app/server/somni/quiz/rpc.py +++ b/app/server/somni/quiz/rpc.py @@ -2,10 +2,9 @@ from __future__ import annotations +import json from typing import TYPE_CHECKING, Any -from google.protobuf.json_format import ParseDict - from app.core.exceptions import ServiceNotReadyError from app.server.errors import abort_from_app_error, abort_invalid, run_rpc_call from app.uburnode_grpc.grpc_gen import uburnode_somni_pb2, uburnode_somni_pb2_grpc @@ -36,20 +35,17 @@ async def _require(self, context) -> QuizService: def _to_res(payload: dict[str, Any]) -> uburnode_somni_pb2.GetAnswerRes: - res = uburnode_somni_pb2.GetAnswerRes() - for item in payload.get("answers") or []: - res.answers.append(_to_item(item if isinstance(item, dict) else {})) - return res - - -def _to_item(item: dict[str, Any]) -> uburnode_somni_pb2.AnswerItem: - answer = uburnode_somni_pb2.AnswerItem( - question_id=str(item.get("question_id") or ""), - input_type=str(item.get("input_type") or ""), - title=str(item.get("title") or ""), - extra_input=str(item.get("extra_input") or ""), + items = [ + { + "question_id": str(item.get("question_id") or ""), + "input_type": str(item.get("input_type") or ""), + "title": str(item.get("title") or ""), + "value": item.get("value"), + "extra_input": str(item.get("extra_input") or ""), + } + for item in (payload.get("answers") or []) + if isinstance(item, dict) + ] + return uburnode_somni_pb2.GetAnswerRes( + answers=json.dumps(items, ensure_ascii=False, separators=(",", ":")), ) - raw = item.get("value") - if raw is not None: - ParseDict(raw, answer.value) - return answer diff --git a/app/uburnode_grpc/grpc_gen/uburnode_somni_pb2.py b/app/uburnode_grpc/grpc_gen/uburnode_somni_pb2.py index 9edcc40..e9c8ae2 100644 --- a/app/uburnode_grpc/grpc_gen/uburnode_somni_pb2.py +++ b/app/uburnode_grpc/grpc_gen/uburnode_somni_pb2.py @@ -25,7 +25,7 @@ from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14uburnode_somni.proto\x12\x11uburnode.somni.v1\x1a\x1cgoogle/protobuf/struct.proto\".\n\x0cGetAnswerReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x11\n\tanswer_id\x18\x02 \x01(\t\"\x94\x01\n\nAnswerItem\x12\x13\n\x0bquestion_id\x18\x01 \x01(\t\x12\x12\n\ninput_type\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12%\n\x05value\x18\x05 \x01(\x0b\x32\x16.google.protobuf.Value\x12\x13\n\x0b\x65xtra_input\x18\x06 \x01(\tJ\x04\x08\x04\x10\x05R\x04tagsR\x06values\">\n\x0cGetAnswerRes\x12.\n\x07\x61nswers\x18\x01 \x03(\x0b\x32\x1d.uburnode.somni.v1.AnswerItem\"1\n\rReportDateReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0brecord_date\x18\x02 \x01(\t\"\x0f\n\rGetSummaryRes\"\x0e\n\x0cGetEventsRes\"\x13\n\x11GetEnvironmentRes\"\x11\n\x0fGetStructureRes\"\x14\n\x12GetSleepQualityRes\"\xc1\x01\n\x0bGetAudioReq\x12\x11\n\x04page\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\tpage_size\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12\x16\n\tfetch_all\x18\x03 \x01(\x08H\x02\x88\x01\x01\x12\x17\n\nquery_text\x18\x04 \x01(\tH\x03\x88\x01\x01\x12\x15\n\x08tag_code\x18\x05 \x01(\tH\x04\x88\x01\x01\x42\x07\n\x05_pageB\x0c\n\n_page_sizeB\x0c\n\n_fetch_allB\r\n\x0b_query_textB\x0b\n\t_tag_code\"O\n\x08PageInfo\x12\x0c\n\x04page\x18\x01 \x01(\x05\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\r\n\x05total\x18\x03 \x01(\x05\x12\x13\n\x0btotal_pages\x18\x04 \x01(\x05\"d\n\x0bGetAudioRes\x12*\n\tmaterials\x18\x01 \x03(\x0b\x32\x17.google.protobuf.Struct\x12)\n\x04page\x18\x02 \x01(\x0b\x32\x1b.uburnode.somni.v1.PageInfo\"\x10\n\x0eGetAudioTagReq\"H\n\x0bTagDictItem\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0f\n\x07name_en\x18\x04 \x01(\t\">\n\x0eGetAudioTagRes\x12,\n\x04tags\x18\x01 \x03(\x0b\x32\x1e.uburnode.somni.v1.TagDictItem\"\x0b\n\tGetHotReq\"\x0b\n\tGetHotRes2\\\n\x0bQuizService\x12M\n\tGetAnswer\x12\x1f.uburnode.somni.v1.GetAnswerReq\x1a\x1f.uburnode.somni.v1.GetAnswerRes2\xbd\x03\n\rReportService\x12P\n\nGetSummary\x12 .uburnode.somni.v1.ReportDateReq\x1a .uburnode.somni.v1.GetSummaryRes\x12N\n\tGetEvents\x12 .uburnode.somni.v1.ReportDateReq\x1a\x1f.uburnode.somni.v1.GetEventsRes\x12X\n\x0eGetEnvironment\x12 .uburnode.somni.v1.ReportDateReq\x1a$.uburnode.somni.v1.GetEnvironmentRes\x12T\n\x0cGetStructure\x12 .uburnode.somni.v1.ReportDateReq\x1a\".uburnode.somni.v1.GetStructureRes\x12Z\n\x0fGetSleepQuality\x12 .uburnode.somni.v1.ReportDateReq\x1a%.uburnode.somni.v1.GetSleepQualityRes2\xf5\x01\n\x0c\x41udioService\x12J\n\x08GetAudio\x12\x1e.uburnode.somni.v1.GetAudioReq\x1a\x1e.uburnode.somni.v1.GetAudioRes\x12S\n\x0bGetAudioTag\x12!.uburnode.somni.v1.GetAudioTagReq\x1a!.uburnode.somni.v1.GetAudioTagRes\x12\x44\n\x06GetHot\x12\x1c.uburnode.somni.v1.GetHotReq\x1a\x1c.uburnode.somni.v1.GetHotResb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14uburnode_somni.proto\x12\x11uburnode.somni.v1\x1a\x1cgoogle/protobuf/struct.proto\".\n\x0cGetAnswerReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x11\n\tanswer_id\x18\x02 \x01(\t\"\x1f\n\x0cGetAnswerRes\x12\x0f\n\x07\x61nswers\x18\x01 \x01(\t\"1\n\rReportDateReq\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x13\n\x0brecord_date\x18\x02 \x01(\t\"\x0f\n\rGetSummaryRes\"\x0e\n\x0cGetEventsRes\"\x13\n\x11GetEnvironmentRes\"\x11\n\x0fGetStructureRes\"\x14\n\x12GetSleepQualityRes\"\xc1\x01\n\x0bGetAudioReq\x12\x11\n\x04page\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\tpage_size\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12\x16\n\tfetch_all\x18\x03 \x01(\x08H\x02\x88\x01\x01\x12\x17\n\nquery_text\x18\x04 \x01(\tH\x03\x88\x01\x01\x12\x15\n\x08tag_code\x18\x05 \x01(\tH\x04\x88\x01\x01\x42\x07\n\x05_pageB\x0c\n\n_page_sizeB\x0c\n\n_fetch_allB\r\n\x0b_query_textB\x0b\n\t_tag_code\"O\n\x08PageInfo\x12\x0c\n\x04page\x18\x01 \x01(\x05\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\r\n\x05total\x18\x03 \x01(\x05\x12\x13\n\x0btotal_pages\x18\x04 \x01(\x05\"d\n\x0bGetAudioRes\x12*\n\tmaterials\x18\x01 \x03(\x0b\x32\x17.google.protobuf.Struct\x12)\n\x04page\x18\x02 \x01(\x0b\x32\x1b.uburnode.somni.v1.PageInfo\"\x10\n\x0eGetAudioTagReq\"H\n\x0bTagDictItem\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0f\n\x07name_en\x18\x04 \x01(\t\">\n\x0eGetAudioTagRes\x12,\n\x04tags\x18\x01 \x03(\x0b\x32\x1e.uburnode.somni.v1.TagDictItem\"\x0b\n\tGetHotReq\"\x0b\n\tGetHotRes2\\\n\x0bQuizService\x12M\n\tGetAnswer\x12\x1f.uburnode.somni.v1.GetAnswerReq\x1a\x1f.uburnode.somni.v1.GetAnswerRes2\xbd\x03\n\rReportService\x12P\n\nGetSummary\x12 .uburnode.somni.v1.ReportDateReq\x1a .uburnode.somni.v1.GetSummaryRes\x12N\n\tGetEvents\x12 .uburnode.somni.v1.ReportDateReq\x1a\x1f.uburnode.somni.v1.GetEventsRes\x12X\n\x0eGetEnvironment\x12 .uburnode.somni.v1.ReportDateReq\x1a$.uburnode.somni.v1.GetEnvironmentRes\x12T\n\x0cGetStructure\x12 .uburnode.somni.v1.ReportDateReq\x1a\".uburnode.somni.v1.GetStructureRes\x12Z\n\x0fGetSleepQuality\x12 .uburnode.somni.v1.ReportDateReq\x1a%.uburnode.somni.v1.GetSleepQualityRes2\xf5\x01\n\x0c\x41udioService\x12J\n\x08GetAudio\x12\x1e.uburnode.somni.v1.GetAudioReq\x1a\x1e.uburnode.somni.v1.GetAudioRes\x12S\n\x0bGetAudioTag\x12!.uburnode.somni.v1.GetAudioTagReq\x1a!.uburnode.somni.v1.GetAudioTagRes\x12\x44\n\x06GetHot\x12\x1c.uburnode.somni.v1.GetHotReq\x1a\x1c.uburnode.somni.v1.GetHotResb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -34,42 +34,40 @@ DESCRIPTOR._loaded_options = None _globals['_GETANSWERREQ']._serialized_start=73 _globals['_GETANSWERREQ']._serialized_end=119 - _globals['_ANSWERITEM']._serialized_start=122 - _globals['_ANSWERITEM']._serialized_end=270 - _globals['_GETANSWERRES']._serialized_start=272 - _globals['_GETANSWERRES']._serialized_end=334 - _globals['_REPORTDATEREQ']._serialized_start=336 - _globals['_REPORTDATEREQ']._serialized_end=385 - _globals['_GETSUMMARYRES']._serialized_start=387 - _globals['_GETSUMMARYRES']._serialized_end=402 - _globals['_GETEVENTSRES']._serialized_start=404 - _globals['_GETEVENTSRES']._serialized_end=418 - _globals['_GETENVIRONMENTRES']._serialized_start=420 - _globals['_GETENVIRONMENTRES']._serialized_end=439 - _globals['_GETSTRUCTURERES']._serialized_start=441 - _globals['_GETSTRUCTURERES']._serialized_end=458 - _globals['_GETSLEEPQUALITYRES']._serialized_start=460 - _globals['_GETSLEEPQUALITYRES']._serialized_end=480 - _globals['_GETAUDIOREQ']._serialized_start=483 - _globals['_GETAUDIOREQ']._serialized_end=676 - _globals['_PAGEINFO']._serialized_start=678 - _globals['_PAGEINFO']._serialized_end=757 - _globals['_GETAUDIORES']._serialized_start=759 - _globals['_GETAUDIORES']._serialized_end=859 - _globals['_GETAUDIOTAGREQ']._serialized_start=861 - _globals['_GETAUDIOTAGREQ']._serialized_end=877 - _globals['_TAGDICTITEM']._serialized_start=879 - _globals['_TAGDICTITEM']._serialized_end=951 - _globals['_GETAUDIOTAGRES']._serialized_start=953 - _globals['_GETAUDIOTAGRES']._serialized_end=1015 - _globals['_GETHOTREQ']._serialized_start=1017 - _globals['_GETHOTREQ']._serialized_end=1028 - _globals['_GETHOTRES']._serialized_start=1030 - _globals['_GETHOTRES']._serialized_end=1041 - _globals['_QUIZSERVICE']._serialized_start=1043 - _globals['_QUIZSERVICE']._serialized_end=1135 - _globals['_REPORTSERVICE']._serialized_start=1138 - _globals['_REPORTSERVICE']._serialized_end=1583 - _globals['_AUDIOSERVICE']._serialized_start=1586 - _globals['_AUDIOSERVICE']._serialized_end=1831 + _globals['_GETANSWERRES']._serialized_start=121 + _globals['_GETANSWERRES']._serialized_end=152 + _globals['_REPORTDATEREQ']._serialized_start=154 + _globals['_REPORTDATEREQ']._serialized_end=203 + _globals['_GETSUMMARYRES']._serialized_start=205 + _globals['_GETSUMMARYRES']._serialized_end=220 + _globals['_GETEVENTSRES']._serialized_start=222 + _globals['_GETEVENTSRES']._serialized_end=236 + _globals['_GETENVIRONMENTRES']._serialized_start=238 + _globals['_GETENVIRONMENTRES']._serialized_end=257 + _globals['_GETSTRUCTURERES']._serialized_start=259 + _globals['_GETSTRUCTURERES']._serialized_end=276 + _globals['_GETSLEEPQUALITYRES']._serialized_start=278 + _globals['_GETSLEEPQUALITYRES']._serialized_end=298 + _globals['_GETAUDIOREQ']._serialized_start=301 + _globals['_GETAUDIOREQ']._serialized_end=494 + _globals['_PAGEINFO']._serialized_start=496 + _globals['_PAGEINFO']._serialized_end=575 + _globals['_GETAUDIORES']._serialized_start=577 + _globals['_GETAUDIORES']._serialized_end=677 + _globals['_GETAUDIOTAGREQ']._serialized_start=679 + _globals['_GETAUDIOTAGREQ']._serialized_end=695 + _globals['_TAGDICTITEM']._serialized_start=697 + _globals['_TAGDICTITEM']._serialized_end=769 + _globals['_GETAUDIOTAGRES']._serialized_start=771 + _globals['_GETAUDIOTAGRES']._serialized_end=833 + _globals['_GETHOTREQ']._serialized_start=835 + _globals['_GETHOTREQ']._serialized_end=846 + _globals['_GETHOTRES']._serialized_start=848 + _globals['_GETHOTRES']._serialized_end=859 + _globals['_QUIZSERVICE']._serialized_start=861 + _globals['_QUIZSERVICE']._serialized_end=953 + _globals['_REPORTSERVICE']._serialized_start=956 + _globals['_REPORTSERVICE']._serialized_end=1401 + _globals['_AUDIOSERVICE']._serialized_start=1404 + _globals['_AUDIOSERVICE']._serialized_end=1649 # @@protoc_insertion_point(module_scope) diff --git a/proto/uburnode_somni.proto b/proto/uburnode_somni.proto index bfa813d..b2fa765 100644 --- a/proto/uburnode_somni.proto +++ b/proto/uburnode_somni.proto @@ -11,19 +11,9 @@ message GetAnswerReq { string answer_id = 2; // 必传:答卷 _id } -message AnswerItem { - string question_id = 1; - string input_type = 2; - string title = 3; - reserved 4; - reserved "tags"; - reserved "values"; - google.protobuf.Value value = 5; - string extra_input = 6; -} - +// answers 为整份答卷数组的 JSON 文本(与库 answers 同形),客户端一次 JSON.parse。 message GetAnswerRes { - repeated AnswerItem answers = 1; + string answers = 1; } service QuizService { diff --git a/tests/test_grpc_quiz.py b/tests/test_grpc_quiz.py index 5aeec4b..cf4e69c 100644 --- a/tests/test_grpc_quiz.py +++ b/tests/test_grpc_quiz.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from unittest.mock import AsyncMock, MagicMock import grpc @@ -101,21 +102,24 @@ async def test_somni_get_answer_maps_answer_item() -> None: _context(), ) payload = MessageToDict(res, preserving_proto_field_name=True) - assert len(payload["answers"]) == 5 - assert payload["answers"][0]["value"] == { - "option_id": "A", - "option_text": "先生", + answers = json.loads(payload["answers"]) + assert len(answers) == 5 + assert answers[0] == { + "question_id": "q1", + "input_type": "radio", + "title": "您的性别是?", + "value": {"option_id": "A", "option_text": "先生"}, + "extra_input": "", } - assert payload["answers"][1]["value"] == [ + assert "tags" not in answers[0] + assert answers[1]["value"] == [ {"option_id": "A", "option_text": "早期"}, {"option_id": "B", "option_text": "VC"}, ] - assert payload["answers"][2]["value"] == 2 - assert payload["answers"][3]["value"] == "最近压力比较大" - assert payload["answers"][4]["value"] is True - assert "tags" not in payload["answers"][0] - assert "values" not in payload["answers"][0] - + assert answers[2]["value"] == 2 + assert answers[3]["value"] == "最近压力比较大" + assert answers[4]["value"] is True + assert json.loads(res.answers)[0]["title"] == "您的性别是?" @pytest.mark.asyncio async def test_somni_quiz_service_loads_from_collection() -> None: diff --git a/tests/test_uburnode_proto_import.py b/tests/test_uburnode_proto_import.py index 5a2deac..69ce72b 100644 --- a/tests/test_uburnode_proto_import.py +++ b/tests/test_uburnode_proto_import.py @@ -21,8 +21,7 @@ def test_somni_package() -> None: assert hasattr(uburnode_somni_pb2_grpc, "AudioServiceServicer") quiz = uburnode_somni_pb2.DESCRIPTOR.services_by_name["QuizService"] assert quiz.full_name == "uburnode.somni.v1.QuizService" - assert "tags" not in uburnode_somni_pb2.AnswerItem.DESCRIPTOR.fields_by_name - assert "values" not in uburnode_somni_pb2.AnswerItem.DESCRIPTOR.fields_by_name - value_field = uburnode_somni_pb2.AnswerItem.DESCRIPTOR.fields_by_name["value"] - assert value_field.number == 5 - assert value_field.message_type.full_name == "google.protobuf.Value" + assert "AnswerItem" not in uburnode_somni_pb2.DESCRIPTOR.message_types_by_name + answers_field = uburnode_somni_pb2.GetAnswerRes.DESCRIPTOR.fields_by_name["answers"] + assert answers_field.type == answers_field.TYPE_STRING + assert not answers_field.is_repeated