# -*- coding: utf-8 -*- """Part 2: 协作 / 方案卡 / 商机阶段 / 详情子域 / 商机分页(6副本).""" import io, os, 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, page_json, req_form_example, req_query_example, ) # ================================================================ # 协作 (OpportunityCollabController) # ================================================================ print('== 协作 ==') def simple_null_post(seq, folder, filename, name, path, params, kv, desc): d = build_docs(name, 'POST', path, desc, params, req_form_example('POST', path, kv), None, env_json('null')) write_bru(folder, filename, bru_form(seq, name, 'post', path, kv, d)) simple_null_post(1, '商机管理/协作', '关注商机.bru', '关注商机', '/api/opportunity/focus', [('oppId', 'string', '是', '-', '商机ID(Long,序列化为 string)')], [('oppId','1')], '当前登录人关注该商机(详情页/列表关注按钮)。已关注再次调用幂等无副作用。') simple_null_post(2, '商机管理/协作', '取关商机.bru', '取关商机', '/api/opportunity/unfocus', [('oppId', 'string', '是', '-', '商机ID')], [('oppId','1')], '取消关注。未关注调用幂等。') simple_null_post(3, '商机管理/协作', '批量关注.bru', '批量关注', '/api/opportunity/focus-batch', [('ids', 'string[]', '是', '-', '商机ID列表(逗号分隔)')], [('ids','1,2,3')], '批量关注:逐条调 focus,每条幂等。') simple_null_post(4, '商机管理/协作', '上报访问.bru', '上报访问', '/api/opportunity/view-touch', [('oppId', 'string', '是', '-', '商机ID')], [('oppId','1')], '打开商机详情页时前端调用一次,写入访问记录用于「最近访问」视图排序。') # 看板阶段汇总 - 返回类型 Result(服务层 boardStageSummary 未定强类型返回结构,按数据类语义推导:阶段汇总列表) board_summary_docs = build_docs( '看板阶段汇总', 'POST', '/api/opportunity/board/stage-summary', '销售机会看板视图:按阶段模板展开的每一节点,返回商机数量与金额汇总(用于阶段泳道头)。`stageTemplateId` 不传时按当前用户默认模板。', [('stageTemplateId', 'string', '否', '-', '阶段模板版本ID(Long,序列化为 string)')], req_form_example('POST', '/api/opportunity/board/stage-summary', [('stageTemplateId','1')]), [ ('stageId', 'string', '阶段节点ID'), ('stageName', 'string', '阶段名称'), ('count', 'number', '该阶段商机数量'), ('totalAmount', 'number', '该阶段项目金额合计'), ], env_json('[\n {\n "stageId": "1",\n "stageName": "需求确认",\n "count": 3,\n "totalAmount": 3000000\n }\n ]'), ) write_bru('商机管理/协作', '看板阶段汇总.bru', bru_form(5, '看板阶段汇总', 'post', '/api/opportunity/board/stage-summary', [('stageTemplateId','1')], board_summary_docs)) # 看板阶段卡片 board_cards_docs = build_docs( '看板阶段卡片', 'POST', '/api/opportunity/board/cards', '销售机会看板视图:拉取指定阶段泳道的商机卡片列表(滚动/翻页用 offset+limit,非页码分页)。', [('stageId', 'string', '是', '-', '阶段节点ID'), ('offset', 'number', '否', '0', '偏移量'), ('limit', 'number', '否', '20', '每次拉取条数')], req_form_example('POST', '/api/opportunity/board/cards', [('stageId','1'),('offset','0'),('limit','20')]), [ ('id', 'string', '商机ID'), ('oppName', 'string', '商机名称'), ('primaryCustomerNameSnapshot', 'string', '主客户名'), ('projectAmount', 'number', '项目金额'), ('ownerNameSnapshot', 'string', '负责人姓名'), ('stageStayDays', 'number', '当前阶段停留天数'), ('lastFollowTime', 'string', '最近跟进时间'), ], env_json('[\n {\n "id": "1",\n "oppName": "示例商机",\n "primaryCustomerNameSnapshot": "示例客户",\n "projectAmount": 1000000,\n "ownerNameSnapshot": "张三",\n "stageStayDays": 3,\n "lastFollowTime": "2024-01-02 10:00:00"\n }\n ]'), ) write_bru('商机管理/协作', '看板阶段卡片.bru', bru_form(6, '看板阶段卡片', 'post', '/api/opportunity/board/cards', [('stageId','1'),('offset','0'),('limit','20')], board_cards_docs)) # ================================================================ # 方案卡 (OpportunitySchemeCardController) # ================================================================ print('== 方案卡 ==') SCHEME_CARD_ROWS = [ ('id', 'string', '方案卡ID'), ('createTime', 'string', '创建时间'), ('updateTime', 'string', '更新时间'), ('oppId', 'string', '所属商机ID'), ('customerId', 'string', '关联客户ID'), ('customerNameSnapshot', 'string', '客户名快照'), ('templateId', 'string', '绑定模板版本行ID'), ('templateName', 'string', '模板名称'), ('templateVersion', 'string', '模板版本号'), ('cardStatus', 'number', '卡状态:1草稿 2已提交 3已推送'), ('editable', 'boolean', '可编辑标记(cardStatus<3 可编辑)'), ('schemeBudget', 'number', '方案预算金额'), ('biddingForm', 'string', '招标形式字典 code'), ('enterStage', 'string', '介入阶段字典 code'), ] SCHEME_CARD_ONE = ( '{\n' ' "id": "1",\n' ' "createTime": "2024-01-01 12:00:00",\n' ' "updateTime": "2024-01-01 12:00:00",\n' ' "oppId": "1",\n' ' "customerId": "1",\n' ' "customerNameSnapshot": "示例客户",\n' ' "templateId": "1",\n' ' "templateName": "标准方案模板",\n' ' "templateVersion": "v1",\n' ' "cardStatus": 1,\n' ' "editable": true,\n' ' "schemeBudget": 800000,\n' ' "biddingForm": "bid_01",\n' ' "enterStage": "stage_01"\n' ' }' ) docs = build_docs( '方案卡列表', 'GET', '/api/opportunity/scheme-card/list', '一商机多张方案卡分组展示。无卡时返回空数组,前端渲染「未创建」态。', [('oppId','string','是','-','商机ID')], req_query_example('GET','/api/opportunity/scheme-card/list',[('oppId','1')]), SCHEME_CARD_ROWS, env_json(f'[\n{SCHEME_CARD_ONE}\n ]'), ) write_bru('商机管理/方案卡', '方案卡列表.bru', bru_form(1, '方案卡列表', 'get', '/api/opportunity/scheme-card/list', [('oppId','1')], docs)) # 方案卡详情 SC_DETAIL_ROWS = SCHEME_CARD_ROWS + [ ('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 = ( '{\n' ' "id": "1",\n' ' "createTime": "2024-01-01 12:00:00",\n' ' "updateTime": "2024-01-01 12:00:00",\n' ' "oppId": "1",\n' ' "customerId": "1",\n' ' "customerNameSnapshot": "示例客户",\n' ' "templateId": "1",\n' ' "templateName": "标准方案模板",\n' ' "templateVersion": "v1",\n' ' "cardStatus": 1,\n' ' "editable": true,\n' ' "schemeBudget": 800000,\n' ' "biddingForm": "bid_01",\n' ' "enterStage": "stage_01",\n' ' "fields": [\n' ' {\n' ' "seqNo": 1,\n' ' "fieldKey": "requirement_summary",\n' ' "fieldName": "需求概述",\n' ' "fieldType": "TEXT",\n' ' "isRequired": 1,\n' ' "fieldValue": "示例填写内容"\n' ' }\n' ' ]\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_ROWS, env_json(SC_DETAIL_JSON), ) write_bru('商机管理/方案卡', '方案卡详情.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(新建方案卡传入 templateId 字段)'), ('templateName', 'string', '模板名称'), ('templateVersion', 'string', '模板版本号'), ], env_json('[\n {\n "templateId": "1",\n "templateName": "标准方案模板",\n "templateVersion": "v1"\n }\n ]'), ) write_bru('商机管理/方案卡', '可选模板版本.bru', bru_form(3, '可选模板版本', 'get', '/api/opportunity/scheme-card/selectable-templates', [('oppId','1')], docs)) # 保存方案卡 save_params = [ ('id','string','否','-','方案卡ID(编辑时必填,新建不传)'), ('oppId','string','是','-','所属商机ID'), ('customerId','string','是','-','关联客户ID(须为本商机的关联客户;编辑时不可更换)'), ('templateId','string','条件','-','绑定模板版本行ID(新建必填,编辑续用锁定版本可不传)'), ('schemeBudget','number','否','-','方案预算金额'), ('biddingForm','string','否','-','招标形式字典 code'), ('enterStage','string','否','-','介入阶段字典 code'), ('valuesJson','string','否','-','动态字段值 JSON 字符串(复杂字段 json-string-in-form 约定);格式:`[{"fieldKey":"...","fieldValue":"..."}]`'), ] save_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` 表单字段上送,随保存全量替换值表。', save_params, req_form_example('POST','/api/opportunity/scheme-card/save',save_kv), '方案卡ID(Long,序列化为 string)', env_json('"1"'), ) write_bru('商机管理/方案卡', '保存方案卡.bru', bru_form(4, '保存方案卡', 'post', '/api/opportunity/scheme-card/save', save_kv, docs)) # 提交方案卡 simple_null_post(5, '商机管理/方案卡', '提交方案卡.bru', '提交方案卡', '/api/opportunity/scheme-card/submit', [('id','string','是','-','方案卡ID')], [('id','1')], '点击「确认」按钮 → 卡状态由 草稿 变为 已提交;提交前校验模板必填字段。') # ================================================================ # 商机阶段 (OpportunityStageController) # ================================================================ print('== 商机阶段 ==') STAGE_NODE_ROWS = [ ('oppId', 'string', '商机ID'), ('oppStatus', 'number', '商机状态(轴1)'), ('stageTemplateId', 'string', '绑定阶段模板版本行ID'), ('stageTemplateVersion', 'string', '阶段模板版本号'), ('currentStageId', 'string', '当前阶段节点ID'), ('currentWorkGoal', 'string', '当前节点工作目标(推进指引区展示)'), ('nodes', 'array', '有序节点列表(按 seq_no 升序,末位为固定节点「已转项目」)'), ('nodes[].nodeId', 'string', '节点ID'), ('nodes[].seqNo', 'number', '阶段序号'), ('nodes[].stageDictCode', 'string', '阶段字典值'), ('nodes[].customNodeName', 'string', '自定义节点名称(空则前端带字典名)'), ('nodes[].workGoal', 'string', '阶段工作目标'), ('nodes[].isFixed', 'number', '固定节点:1固定(已转项目)0普通'), ('nodes[].nodeState', 'string', '进度状态:COMPLETED已完成 / CURRENT当前 / PENDING未到达'), ] STAGE_PROGRESS_JSON = ( '{\n' ' "oppId": "1",\n' ' "oppStatus": 2,\n' ' "stageTemplateId": "1",\n' ' "stageTemplateVersion": "v1",\n' ' "currentStageId": "2",\n' ' "currentWorkGoal": "确认客户预算与决策链",\n' ' "nodes": [\n' ' {\n' ' "nodeId": "1",\n' ' "seqNo": 1,\n' ' "stageDictCode": "stage_01",\n' ' "customNodeName": null,\n' ' "workGoal": "初步接触",\n' ' "isFixed": 0,\n' ' "nodeState": "COMPLETED"\n' ' }\n' ' ]\n' ' }' ) docs = build_docs( '阶段推进指引', 'GET', '/api/opportunity/stage/progress', '商机概览页三态进度展示:已完成 / 当前 / 未完成 + 当前节点工作目标。按商机绑定的模板版本(锁版本,发新版不迁移)展开有序节点;节点展示名 = customNodeName 优先,空则前端按 stageDictCode 带字典名。', [('oppId','string','是','-','商机ID')], req_query_example('GET','/api/opportunity/stage/progress',[('oppId','1')]), STAGE_NODE_ROWS, env_json(STAGE_PROGRESS_JSON), ) write_bru('商机管理/商机阶段', '阶段推进指引.bru', bru_form(1, '阶段推进指引', 'get', '/api/opportunity/stage/progress', [('oppId','1')], docs)) simple_null_post(2, '商机管理/商机阶段', '切换阶段.bru', '切换阶段', '/api/opportunity/stage/switch', [('oppId','string','是','-','商机ID'), ('toStageId','string','是','-','目标阶段节点ID(须属商机绑定的模板版本且非固定节点)'), ('remark','string','否','-','推进说明')], [('oppId','1'),('toStageId','3'),('remark','客户已确认预算')], '自由切换阶段:允许连跳 / 回退。「已转项目」固定节点不可手动切(由业务流程触发)。') # 阶段历史 STAGE_HIST_ROWS = [ ('id','string','历史记录ID'), ('createTime','string','创建时间'), ('updateTime','string','更新时间'), ('oppId','string','商机ID'), ('fromStageId','string','原阶段节点ID'), ('toStageId','string','目标阶段节点ID'), ('operatorUserId','string','操作人ID(系统动作为 0)'), ('operateTime','string','操作时间'), ('remark','string','推进说明'), ] STAGE_HIST_JSON = ( '[\n' ' {\n' ' "id": "1",\n' ' "createTime": "2024-01-01 12:00:00",\n' ' "updateTime": "2024-01-01 12:00:00",\n' ' "oppId": "1",\n' ' "fromStageId": "1",\n' ' "toStageId": "2",\n' ' "operatorUserId": "1",\n' ' "operateTime": "2024-01-01 12:00:00",\n' ' "remark": "推进说明示例"\n' ' }\n' ' ]' ) docs = build_docs( '阶段推进历史', 'GET', '/api/opportunity/stage/history', '按操作时间倒序返回该商机的全部阶段推进记录(人工推进 + 系统动作合并展示)。', [('oppId','string','是','-','商机ID')], req_query_example('GET','/api/opportunity/stage/history',[('oppId','1')]), STAGE_HIST_ROWS, env_json(STAGE_HIST_JSON), ) write_bru('商机管理/商机阶段', '阶段推进历史.bru', bru_form(3, '阶段推进历史', 'get', '/api/opportunity/stage/history', [('oppId','1')], docs)) print('part2 done — 协作/方案卡/商机阶段 完成')