# -*- coding: utf-8 -*- """第5批:商机详情各 Tab(21 个)""" import io, sys sys.path.insert(0, 'D:/code/crm-backend-matt/.scratch') sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') from bru_lib import write_bru, bru_form, build_docs, env_json, req_form_example, req_query_example BASE = '商机模块/商机详情' # ── 客户 Tab ────────────────────────────────────────────────────────── print('== 客户 ==') CUSTOMER_RESP = [ ('id','string','关联行 ID'), ('opportunityId','string','所属商机 ID'), ('customerId','string','客户 ID'), ('customerNameSnapshot','string','客户名快照'), ('customerRole','string','客户角色字典 code'), ('isPrimaryIntended','number','是否主要意向客户:1是 0否'), ] docs = build_docs( '客户列表', 'GET', '/api/opportunity/customer/list', '商机详情「客户」Tab:返回全部关联客户,其中恰好一个 `isPrimaryIntended=1` 为主要意向客户。', [('oppId','string','是','-','商机 ID')], req_query_example('GET','/api/opportunity/customer/list',[('oppId','1')]), CUSTOMER_RESP, env_json('[\n {"id":"1","opportunityId":"1","customerId":"1","customerNameSnapshot":"示例客户","customerRole":"role_01","isPrimaryIntended":1}\n ]'), ) write_bru(f'{BASE}/客户', '客户列表.bru', bru_form(1,'客户列表','get','/api/opportunity/customer/list',[('oppId','1')],docs)) # ── 跟进 Tab ────────────────────────────────────────────────────────── print('== 跟进 ==') FOLLOW_RESP = [ ('id','string','跟进记录 ID'), ('createTime','string','创建时间(写入时间)'), ('oppId','string','所属商机 ID'), ('followContent','string','跟进内容'), ('followTime','string','跟进时间'), ('followWay','string','跟进方式字典 code'), ('customerId','string','关联客户 ID'), ('contactId','string','客户联系人 ID(可空)'), ('resultTag','string','跟进结果标签字典 code(可空)'), ('nextFollowTime','string','计划下次跟进时间(可空)'), ] FOLLOW_ONE = '{\n "id":"1","createTime":"2024-01-01 12:00:00","oppId":"1","followContent":"已电话沟通预算","followTime":"2024-01-01 12:00:00","followWay":"way_01","customerId":"1","contactId":"1","resultTag":"tag_01","nextFollowTime":"2024-01-05 12:00:00"\n }' FOLLOW_PAGE = env_json('{\n "content":[\n' + FOLLOW_ONE + '\n ],"total":1,"size":10,"current":1,"pages":1,"empty":false\n }') docs = build_docs( '跟进分页','POST','/api/opportunity/follow/page', '商机详情「跟进」Tab:分页拉取跟进记录,默认按创建时间倒序。', [('oppId','string','是','-','商机 ID'),('pageNum','number','否','1','页码'),('pageSize','number','否','10','每页条数')], req_form_example('POST','/api/opportunity/follow/page',[('oppId','1'),('pageNum','1'),('pageSize','10')]), [('content','array','跟进记录数组'),('total','number','总条数'),('size','number','每页条数'),('current','number','当前页'),('pages','number','总页数'),('empty','boolean','是否为空')], FOLLOW_PAGE, ) write_bru(f'{BASE}/跟进','跟进分页.bru',bru_form(1,'跟进分页','post','/api/opportunity/follow/page',[('oppId','1'),('pageNum','1'),('pageSize','10')],docs)) ADD_FOLLOW_KV=[('oppId','1'),('followContent','已电话沟通预算'),('followTime','2024-01-01 12:00:00'),('followWay','way_01'),('customerId','1')] docs = build_docs( '新增跟进','POST','/api/opportunity/follow/add', '写跟进弹窗「确认」提交。保存后刷新商机「最近跟进」与 `lastValidFollowTime`(回收锚点)。与阶段推进不联动。', [('oppId','string','是','-','商机 ID'),('followContent','string','是','-','跟进内容'), ('followTime','string','是','-','跟进时间 yyyy-MM-dd HH:mm:ss'),('followWay','string','是','-','跟进方式字典 code'), ('customerId','string','是','-','关联客户 ID'),('contactId','string','否','-','联系人 ID'), ('resultTag','string','否','-','跟进结果标签字典 code'),('nextFollowTime','string','否','-','计划下次跟进时间')], req_form_example('POST','/api/opportunity/follow/add',ADD_FOLLOW_KV), '新增跟进记录 ID(Long,序列化为 string)', env_json('"1"'), ) write_bru(f'{BASE}/跟进','新增跟进.bru',bru_form(2,'新增跟进','post','/api/opportunity/follow/add',ADD_FOLLOW_KV,docs)) docs = build_docs( '删除跟进','POST','/api/opportunity/follow/delete', '删除跟进记录。`lastValidFollowTime` 删除后不回退重算(防「写了又删」刷锚点)。', [('id','string','是','-','跟进记录 ID')], req_form_example('POST','/api/opportunity/follow/delete',[('id','1')]), None, env_json('null'), ) write_bru(f'{BASE}/跟进','删除跟进.bru',bru_form(3,'删除跟进','post','/api/opportunity/follow/delete',[('id','1')],docs)) # ── 勘察 Tab ────────────────────────────────────────────────────────── print('== 勘察 ==') SURVEY_RESP=[ ('id','string','勘察记录 ID'),('createTime','string','创建时间'), ('oppId','string','所属商机 ID'),('surveyDate','string','现场勘察日期 yyyy-MM-dd'), ('engineerUserId','string','主要工程师用户 ID'),('applyWay','string','申请方式字典 code'), ('surveySeq','string','勘察次数字典 code'),('applyNo','string','申请单号(可空)'), ('surveyDesc','string','勘察补充说明(可空)'), ] SURVEY_ONE='{\n "id":"1","createTime":"2024-01-01 12:00:00","oppId":"1","surveyDate":"2024-01-15","engineerUserId":"1","applyWay":"apply_01","surveySeq":"seq_01","applyNo":"SS001","surveyDesc":"现场符合要求"\n }' SURVEY_PAGE=env_json('{\n "content":[\n'+SURVEY_ONE+'\n ],"total":1,"size":10,"current":1,"pages":1,"empty":false\n }') docs=build_docs('勘察分页','POST','/api/opportunity/site-survey/page', '商机详情「勘察」Tab:分页拉取现场勘察记录(多次勘察保留历史)。', [('oppId','string','是','-','商机 ID'),('pageNum','number','否','1','页码'),('pageSize','number','否','10','每页条数')], req_form_example('POST','/api/opportunity/site-survey/page',[('oppId','1'),('pageNum','1'),('pageSize','10')]), [('content','array','勘察记录数组'),('total','number','总条数'),('size','number','每页条数'),('current','number','当前页'),('pages','number','总页数'),('empty','boolean','是否为空')], SURVEY_PAGE) write_bru(f'{BASE}/勘察','勘察分页.bru',bru_form(1,'勘察分页','post','/api/opportunity/site-survey/page',[('oppId','1'),('pageNum','1'),('pageSize','10')],docs)) ADD_SURVEY_KV=[('oppId','1'),('surveyDate','2024-01-15'),('engineerUserId','1'),('applyWay','apply_01'),('surveySeq','seq_01')] docs=build_docs('新增勘察','POST','/api/opportunity/site-survey/add', '新建现场勘察记录。附件走统一附件表(bizType=SITE_SURVEY,bizId=本记录 id)。', [('oppId','string','是','-','商机 ID'),('surveyDate','string','是','-','勘察日期 yyyy-MM-dd'), ('engineerUserId','string','是','-','主要工程师用户 ID'),('applyWay','string','是','-','申请方式字典 code'), ('surveySeq','string','是','-','勘察次数字典 code'),('applyNo','string','否','-','申请单号'),('surveyDesc','string','否','-','补充说明')], req_form_example('POST','/api/opportunity/site-survey/add',ADD_SURVEY_KV), '新增勘察记录 ID', env_json('"1"')) write_bru(f'{BASE}/勘察','新增勘察.bru',bru_form(2,'新增勘察','post','/api/opportunity/site-survey/add',ADD_SURVEY_KV,docs)) docs=build_docs('删除勘察','POST','/api/opportunity/site-survey/delete','逻辑删除勘察记录。', [('id','string','是','-','勘察记录 ID')], req_form_example('POST','/api/opportunity/site-survey/delete',[('id','1')]), None,env_json('null')) write_bru(f'{BASE}/勘察','删除勘察.bru',bru_form(3,'删除勘察','post','/api/opportunity/site-survey/delete',[('id','1')],docs)) # ── 附件 Tab ────────────────────────────────────────────────────────── print('== 附件 ==') ATT_RESP=[ ('id','string','附件行 ID'),('createTime','string','上传时间'), ('oppId','string','所属商机 ID'),('bizType','string','业务归属:OPP/FOLLOW_UP/SITE_SURVEY/SCHEME_CARD'), ('bizId','string','业务对象 ID'),('materialType','string','资料类型字典 code'), ('fileId','string','文件 ID(引用 crm-file)'),('remark','string','附件说明'), ] docs=build_docs('附件列表','GET','/api/opportunity/attachment/list', '商机详情「附件」Tab:按 `bizType` 过滤;不传则返回该商机全部附件。', [('oppId','string','是','-','商机 ID'),('bizType','string','否','-','业务归属类型过滤')], req_query_example('GET','/api/opportunity/attachment/list',[('oppId','1'),('bizType','OPP')]), ATT_RESP, env_json('[\n {"id":"1","createTime":"2024-01-01 12:00:00","oppId":"1","bizType":"OPP","bizId":"1","materialType":"mat_01","fileId":"1","remark":"客户需求文档"}\n ]')) write_bru(f'{BASE}/附件','附件列表.bru',bru_form(1,'附件列表','get','/api/opportunity/attachment/list',[('oppId','1'),('bizType','OPP')],docs)) ADD_ATT_KV=[('oppId','1'),('fileId','1'),('bizType','OPP'),('bizId','1'),('materialType','mat_01')] docs=build_docs('新增附件','POST','/api/opportunity/attachment/add', '将已上传到 crm-file 的文件挂到商机某业务对象下。跟进附件(bizType=FOLLOW_UP)应用层限 10 个/条。', [('oppId','string','是','-','商机 ID'),('fileId','string','是','-','文件 ID(先调 crm-file 上传获取)'), ('bizType','string','否','-','业务归属类型'),('bizId','string','否','-','业务对象 ID'), ('materialType','string','否','-','资料类型字典 code'),('remark','string','否','-','附件说明')], req_form_example('POST','/api/opportunity/attachment/add',ADD_ATT_KV), '新增附件行 ID',env_json('"1"')) write_bru(f'{BASE}/附件','新增附件.bru',bru_form(2,'新增附件','post','/api/opportunity/attachment/add',ADD_ATT_KV,docs)) docs=build_docs('删除附件','POST','/api/opportunity/attachment/delete', '逻辑删除附件行;文件本体不动,由 crm-file 侧独立管理生命周期。', [('id','string','是','-','附件行 ID')], req_form_example('POST','/api/opportunity/attachment/delete',[('id','1')]), None,env_json('null')) write_bru(f'{BASE}/附件','删除附件.bru',bru_form(3,'删除附件','post','/api/opportunity/attachment/delete',[('id','1')],docs)) # ── 团队 Tab ────────────────────────────────────────────────────────── print('== 团队 ==') TEAM_RESP=[ ('id','string','团队成员行 ID'),('createTime','string','加入团队时间'), ('opportunityId','string','所属商机 ID'),('userId','string','成员用户 ID'), ('userNameSnapshot','string','成员姓名快照'), ('projectRole','string','项目角色字典 code(纯标签,不带权限)'), ('permission','number','行级权限档:1只读 2可写跟进 3可推进节点 4读写'), ] docs=build_docs('团队成员列表','GET','/api/opportunity/team/list', '商机详情「团队」Tab:返回全部团队成员(含负责人)。团队白名单绕开 DataScope 部门天花板,为「我参与的」视图与详情读授权提供依据。', [('oppId','string','是','-','商机 ID')], req_query_example('GET','/api/opportunity/team/list',[('oppId','1')]), TEAM_RESP, env_json('[\n {"id":"1","createTime":"2024-01-01 12:00:00","opportunityId":"1","userId":"1","userNameSnapshot":"张三","projectRole":"role_owner","permission":4}\n ]')) write_bru(f'{BASE}/团队','团队成员列表.bru',bru_form(1,'团队成员列表','get','/api/opportunity/team/list',[('oppId','1')],docs)) # ── 日志 Tab ────────────────────────────────────────────────────────── print('== 日志 ==') OPLOG_RESP=[ ('id','string','日志 ID'),('createTime','string','创建时间'), ('oppId','string','商机 ID'), ('opKind','string','记录形态:FIELD_CHANGE / ROW_ADD / ROW_DELETE'), ('logType','string','业务类别:FIELD_CHANGE/STATUS_FLOW/STAGE_FLOW/ATTACHMENT/SCHEME_PUSH/ROW_CHANGE/SYSTEM_INTERNAL'), ('entityName','string','变更实体对象'), ('fieldName','string','变更字段名(FIELD_CHANGE 时有意义)'), ('oldValue','string','旧值'),('newValue','string','新值'), ('bizRef','string','被操作行业务标识(ROW_ADD/ROW_DELETE 时的可读标识)'), ('opDesc','string','操作说明'), ('opSource','string','操作来源:SYSTEM / MANUAL'), ('opTime','string','操作时间'),('opUserId','string','操作人 ID'), ] OPLOG_ONE='{\n "id":"1","createTime":"2024-01-01 12:00:00","oppId":"1","opKind":"FIELD_CHANGE","logType":"FIELD_CHANGE","entityName":"opportunity","fieldName":"projectAmount","oldValue":"500000","newValue":"800000","bizRef":null,"opDesc":"修改项目金额","opSource":"MANUAL","opTime":"2024-01-01 12:00:00","opUserId":"1"\n }' OPLOG_PAGE=env_json('{\n "content":[\n'+OPLOG_ONE+'\n ],"total":1,"size":10,"current":1,"pages":1,"empty":false\n }') docs=build_docs('操作日志分页','POST','/api/opportunity/oplog/page', '商机详情「日志」Tab:字段级审计日志,按操作时间倒序。只增不删。', [('oppId','string','是','-','商机 ID'),('pageNum','number','否','1','页码'),('pageSize','number','否','10','每页条数')], req_form_example('POST','/api/opportunity/oplog/page',[('oppId','1'),('pageNum','1'),('pageSize','10')]), [('content','array','日志记录数组'),('total','number','总条数'),('size','number','每页条数'),('current','number','当前页'),('pages','number','总页数'),('empty','boolean','是否为空')], OPLOG_PAGE) write_bru(f'{BASE}/日志','操作日志分页.bru',bru_form(1,'操作日志分页','post','/api/opportunity/oplog/page',[('oppId','1'),('pageNum','1'),('pageSize','10')],docs)) # ── 方案卡 Tab ──────────────────────────────────────────────────────── print('== 方案卡 ==') SC_RESP=[ ('id','string','方案卡 ID'),('createTime','string','创建时间'), ('oppId','string','所属商机 ID'),('customerId','string','关联客户 ID'), ('customerNameSnapshot','string','客户名快照'), ('templateId','string','绑定模板版本行 ID'),('templateName','string','模板名称'), ('cardStatus','number','卡状态:1草稿 2已提交 3已推送'), ('editable','boolean','可编辑标记(cardStatus<3 可编辑)'), ('schemeBudget','number','方案预算金额'), ('biddingForm','string','招标形式字典 code'),('enterStage','string','介入阶段字典 code'), ] SC_ONE='{\n "id":"1","createTime":"2024-01-01 12:00:00","oppId":"1","customerId":"1","customerNameSnapshot":"示例客户","templateId":"1","templateName":"标准方案模板","cardStatus":1,"editable":true,"schemeBudget":800000,"biddingForm":"bid_01","enterStage":"stage_01"\n }' docs=build_docs('方案卡列表','GET','/api/opportunity/scheme-card/list', '商机详情「方案卡」Tab:一商机多张方案卡分组展示。无卡时返回空数组。', [('oppId','string','是','-','商机 ID')], req_query_example('GET','/api/opportunity/scheme-card/list',[('oppId','1')]), SC_RESP, env_json('[\n'+SC_ONE+'\n ]')) write_bru(f'{BASE}/方案卡','方案卡列表.bru',bru_form(1,'方案卡列表','get','/api/opportunity/scheme-card/list',[('oppId','1')],docs)) SC_DETAIL_RESP=SC_RESP+[ ('fields','array','字段项有序列表'), ('fields[].seqNo','number','序号'),('fields[].fieldKey','string','字段键'), ('fields[].fieldName','string','字段名'),('fields[].fieldType','string','字段类型'), ('fields[].isRequired','number','必填:1是 0否'),('fields[].fieldValue','string','已填值'), ] SC_DETAIL_JSON=env_json('{\n "id":"1","createTime":"2024-01-01 12:00:00","oppId":"1","customerId":"1","customerNameSnapshot":"示例客户","templateId":"1","templateName":"标准方案模板","cardStatus":1,"editable":true,"schemeBudget":800000,"biddingForm":"bid_01","enterStage":"stage_01","fields":[{"seqNo":1,"fieldKey":"requirement_summary","fieldName":"需求概述","fieldType":"TEXT","isRequired":1,"fieldValue":"示例填写内容"}]\n }') docs=build_docs('方案卡详情','GET','/api/opportunity/scheme-card/detail', '按绑定模板版本回显字段项有序列表 + 已填值。结构化字段(招标形式/介入阶段/预算)从主表回填,非结构化字段从值表回填。', [('id','string','是','-','方案卡 ID')], req_query_example('GET','/api/opportunity/scheme-card/detail',[('id','1')]), SC_DETAIL_RESP, SC_DETAIL_JSON) write_bru(f'{BASE}/方案卡','方案卡详情.bru',bru_form(2,'方案卡详情','get','/api/opportunity/scheme-card/detail',[('id','1')],docs)) docs=build_docs('可选模板版本','GET','/api/opportunity/scheme-card/selectable-templates', '新建方案卡「选模板」下拉数据源:发布中且适用范围命中商机所属部门的模板版本。空列表 = 建卡入口置灰。', [('oppId','string','是','-','商机 ID')], req_query_example('GET','/api/opportunity/scheme-card/selectable-templates',[('oppId','1')]), [('templateId','string','模板版本行 ID'),('templateName','string','模板名称'),('templateVersion','string','模板版本号')], env_json('[\n {"templateId":"1","templateName":"标准方案模板","templateVersion":"v1"}\n ]')) write_bru(f'{BASE}/方案卡','可选模板版本.bru',bru_form(3,'可选模板版本','get','/api/opportunity/scheme-card/selectable-templates',[('oppId','1')],docs)) SAVE_SC_KV=[('oppId','1'),('customerId','1'),('templateId','1'),('schemeBudget','800000'),('biddingForm','bid_01'),('enterStage','stage_01'),('valuesJson','[{"fieldKey":"requirement_summary","fieldValue":"示例填写内容"}]')] docs=build_docs('保存方案卡','POST','/api/opportunity/scheme-card/save', '新建草稿/编辑保存。新建须传 `templateId`;编辑时模板版本已锁不重传。动态字段值以 `valuesJson` 表单字段上送(json-string-in-form 约定),随保存全量替换值表。', [('id','string','否','-','方案卡 ID(编辑时必填,新建不传)'), ('oppId','string','是','-','商机 ID'),('customerId','string','是','-','关联客户 ID'), ('templateId','string','条件','-','绑定模板版本行 ID(新建必填)'), ('schemeBudget','number','否','-','方案预算金额'), ('biddingForm','string','否','-','招标形式字典 code'),('enterStage','string','否','-','介入阶段字典 code'), ('valuesJson','string','否','-','动态字段值 JSON 字符串:`[{"fieldKey":"...","fieldValue":"..."}]`')], req_form_example('POST','/api/opportunity/scheme-card/save',SAVE_SC_KV), '方案卡 ID(Long,序列化为 string)',env_json('"1"')) write_bru(f'{BASE}/方案卡','保存方案卡.bru',bru_form(4,'保存方案卡','post','/api/opportunity/scheme-card/save',SAVE_SC_KV,docs)) docs=build_docs('提交方案卡','POST','/api/opportunity/scheme-card/submit', '点击「确认」→ 卡状态由草稿变为已提交;提交前校验模板必填字段。', [('id','string','是','-','方案卡 ID')], req_form_example('POST','/api/opportunity/scheme-card/submit',[('id','1')]), None,env_json('null')) write_bru(f'{BASE}/方案卡','提交方案卡.bru',bru_form(5,'提交方案卡','post','/api/opportunity/scheme-card/submit',[('id','1')],docs)) print('batch5 done')