# -*- coding: utf-8 -*- """A4 客户管理 Bruno 文档定义表(票 06)。 结构听源码:全部条目逐一核对 9 个 Controller 源码(20260903)+ crm-rule/crm-preference。 params 元组:(名, 类型, 必填1/0, 说明)。desc 初稿=源码 @Operation/@Schema。 示例 id 取 seed-ids.json 实测值。 """ ADMIN = '739564171091247104' BUDDY = '744842318024015872' CID = '750844477165273088' # e2c-全字段-科技 CID_POOL = '750844487390986240' # e2c-公海-甲 CID_TRANSFER = '750844484425613312' # e2c-交割-甲 CTID = '750844500171030528' # 联系人 张关键 TID = '750844495993503744' # 商机 e2c-联动-进行中 # 公共 BaseParam 四件套(所有分页/列表查询继承) BP = [ ('current', 'number', 0, '页码,默认 1(BaseParam)'), ('size', 'number', 0, '每页条数,默认 10(BaseParam)'), ('keyword', 'string', 0, '关键字模糊匹配(BaseParam;列表页按名称/编号等业务列匹配)'), ('orderBy', 'string', 0, '排序列(BaseParam,可空);asc=false 默认倒序'), ] DEFS = [ # ============ A4 客户管理/客户管理(CustomerController 10) ============ dict(dir='A4 客户管理/客户管理', name='新增客户', method='POST', path='/api/customer', body='form', desc='三层查重内建:L2 名称相似返回 needConfirm(未落库),确认后带 confirmSimilar=true 重发;ownerUserId 空=直接进公海并记 enter_pool_time。写端点为隐式表单绑定(ADR-0017)。', params=[ ('customerName', 'string', 1, '客户名称(必填,停输防抖触发相似查重)'), ('customerType', 'string', 1, '客户类型字典 code(customer_type 分组)'), ('provinceCode', 'string', 1, '省国标 code'), ('cityCode', 'string', 1, '市国标 code'), ('districtCode', 'string', 0, '区/县国标 code'), ('industryCode', 'string', 1, '行业一级 code'), ('industryChildCode', 'string', 0, '行业二级 code'), ('customerStarLevel', 'number', 1, '客户星级 1~5'), ('relationStarLevel', 'number', 1, '关系星级 1~5'), ('networkUnits', 'string', 0, '人脉关系单位 JSON 数组(标签名)'), ('ownerUserId', 'string', 0, '销售负责人 ID(空=直接进公海;快照服务端补齐)'), ('companyDecisionMaker', 'string', 0, '公司决策人'), ('isBizNegotiated', 'number', 1, '是否商机勾兑:1是 0否'), ('directorVisitTime', 'string', 0, '总监近期拜访时间 yyyy-MM-dd HH:mm:ss'), ('isChild', 'number', 1, '是否子客户:1是 0否(默认 0)'), ('parentCustomerId', 'string', 0, '父客户 ID(isChild=1 时必填,服务端防环校验)'), ('cooperationSystem', 'string', 0, '合作系统'), ('nonCoopReason', 'string', 0, '不合作原因'), ('joinedPresidentClass', 'number', 0, '是否参加总裁班:1是 0否 未选 null'), ('presidentClassPerson', 'string', 0, '总裁班参加人(joinedPresidentClass=1 时必填)'), ('presidentClassPhone', 'string', 0, '总裁班参加人电话'), ('joinedProductClass', 'number', 0, '是否参加产品经理班:1是 0否 未选 null'), ('productClassPerson', 'string', 0, '产品班参加人(joinedProductClass=1 时必填)'), ('productClassPhone', 'string', 0, '产品班参加人电话'), ('unifiedCreditCode', 'string', 0, '统一社会信用代码(18 位数字或大写字母;撞码 L3 硬拦 67003)'), ('legalRepresentative', 'string', 0, '法定代表人'), ('establishedDate', 'string', 0, '成立时间 yyyy-MM-dd(不得晚于当前日期)'), ('registeredCapital', 'string', 0, '注册资本(文本含单位)'), ('businessScope', 'string', 0, '经营范围'), ('staffSize', 'string', 0, '人员规模'), ('annualRevenue', 'string', 0, '年产值'), ('businessAddress', 'string', 0, '工商注册地址'), ('bizNameFromLookup', 'string', 0, '企查查确认的工商名称(只存不覆盖客户名称)'), ('remark', 'string', 0, '备注(≤500 字)'), ('confirmSimilar', 'boolean', 0, 'L2 名称相似弹窗后用户确认仍要创建(重发时置 true)'), ], errs=['67001 入参非法', '67003 信用代码撞码', '67015 查重设置单例缺失'], resp='CustomerSaveResultDTO 双形态:needConfirm=true + similarHits[](未落库,待确认);或直接返回新客户 id(string)'), dict(dir='A4 客户管理/客户管理', name='编辑客户', method='PUT', path='/api/customer/{id}', body='form', desc='CAS 乐观锁(version 必传,不匹配报 67005);查重排除自身;归属字段(ownerUserId)不可改。', params=[('version', 'number', 1, '乐观锁版本号(GET /{id} 回显值带回;不匹配报 67005)')] + [ ('customerName', 'string', 1, '客户名称(必填)'), ('customerType', 'string', 1, '客户类型字典 code'), ('provinceCode', 'string', 1, '省国标 code'), ('cityCode', 'string', 1, '市国标 code'), ('districtCode', 'string', 0, '区/县国标 code'), ('industryCode', 'string', 1, '行业一级 code'), ('industryChildCode', 'string', 0, '行业二级 code'), ('customerStarLevel', 'number', 1, '客户星级 1~5'), ('relationStarLevel', 'number', 1, '关系星级 1~5'), ('networkUnits', 'string', 0, '人脉关系单位 JSON 数组'), ('companyDecisionMaker', 'string', 0, '公司决策人'), ('isBizNegotiated', 'number', 1, '是否商机勾兑'), ('directorVisitTime', 'string', 0, '总监近期拜访时间'), ('isChild', 'number', 1, '是否子客户(默认 0)'), ('parentCustomerId', 'string', 0, '父客户 ID(isChild=1 时必填)'), ('cooperationSystem', 'string', 0, '合作系统'), ('nonCoopReason', 'string', 0, '不合作原因'), ('joinedPresidentClass', 'number', 0, '是否参加总裁班'), ('presidentClassPerson', 'string', 0, '总裁班参加人'), ('presidentClassPhone', 'string', 0, '总裁班参加人电话'), ('joinedProductClass', 'number', 0, '是否参加产品经理班'), ('productClassPerson', 'string', 0, '产品班参加人'), ('productClassPhone', 'string', 0, '产品班参加人电话'), ('unifiedCreditCode', 'string', 0, '统一社会信用代码(18 位)'), ('legalRepresentative', 'string', 0, '法定代表人'), ('establishedDate', 'string', 0, '成立时间(不得晚于当前日期)'), ('registeredCapital', 'string', 0, '注册资本'), ('businessScope', 'string', 0, '经营范围'), ('staffSize', 'string', 0, '人员规模'), ('annualRevenue', 'string', 0, '年产值'), ('businessAddress', 'string', 0, '工商注册地址'), ('bizNameFromLookup', 'string', 0, '企查查确认的工商名称'), ('remark', 'string', 0, '备注(≤500 字)'), ('confirmSimilar', 'boolean', 0, 'L2 名称相似弹窗后确认仍要保存(重发时置 true)'), ], errs=['67001 入参非法', '67002 客户不存在', '67003 信用代码撞码', '67005 CAS 冲突'], resp='CustomerSaveResultDTO(同新增;命中相似时 needConfirm=true)'), dict(dir='A4 客户管理/客户管理', name='客户详情', method='GET', path='/api/customer/{id}', body='none', desc='全字段带出 + 字典名回显;编辑页复用(编辑回显含 version)。', params=[], errs=['67002 客户不存在'], resp='CustomerDetailDTO 全字段(含字典回显名与 version)'), dict(dir='A4 客户管理/客户管理', name='客户分页', method='POST', path='/api/customer/page', body='form', desc='基础分页(keyword/阶段/档案/类型);DataScope 注入可见范围。', params=[('customerStage', 'number', 0, '客户阶段:1 潜在 2 重潜 3 已成交'), ('archiveStatus', 'number', 0, '档案状态:1 有效 2 已归档(null=默认只看有效)'), ('customerType', 'string', 0, '客户类型字典 code')] + BP, errs=[], resp='PageResult(content/total,ID 序列化为 string)'), dict(dir='A4 客户管理/客户管理', name='商机侧客户搜索', method='POST', path='/api/customer/search', body='form', desc='名称/联系人/电话三维模糊 + 类型精确 + 分页;商机新增关联客户数据源;DataScope 收口。', params=[('customerType', 'string', 0, '客户类型字典 code(精确过滤,可空)'), ('archiveStatus', 'number', 0, '档案状态 1 有效/2 已归档(可空=全部)'), ('excludeCustomerIds', 'string', 0, '排除客户 id 集合(商机候选池排除已关联客户,多值重复键)')] + BP, errs=[], resp='PageResult'), dict(dir='A4 客户管理/客户管理', name='快速创建客户', method='POST', path='/api/customer/quick-create', body='form', desc='商机弹窗最小必填集;销售负责人默认当前用户、is_child/is_biz_negotiated 默认否;查重三层内建。已知缺口:QuickCreateDTO 无 confirmSimilar 字段,命中相似返回 needConfirm 但无法「仍要创建」。', params=[('customerName', 'string', 1, '客户名称'), ('customerType', 'string', 1, '客户类型字典 code'), ('provinceCode', 'string', 1, '省份编码'), ('cityCode', 'string', 1, '城市编码'), ('industryCode', 'string', 0, '行业编码'), ('customerStarLevel', 'number', 0, '客户星级'), ('relationStarLevel', 'number', 0, '关系星级')], errs=['67001 入参非法', '67003 信用代码撞码'], resp='CustomerSaveResultDTO(同新增;无 confirmSimilar 重发通道)'), dict(dir='A4 客户管理/客户管理', name='按客户查联系人', method='GET', path='/api/customer/{customerId}/contacts', body='none', desc='商机字段 17 级联数据源;详情页签 2 联系人同用;权限继承客户(无独立 ACL)。', params=[], errs=['67002 客户不存在'], resp='List(字段见 A4-5 联系人/新增联系人)'), dict(dir='A4 客户管理/客户管理', name='名称相似提示', method='GET', path='/api/customer/check-name', body='none', desc='L1 停输防抖调用;ngram 相似阈值 70/80/90;无权命中脱敏为「存在匹配记录」(masked)。', params=[('name', 'string', 1, '待查客户名称'), ('excludeId', 'string', 0, '排除的客户 id(编辑页传自身)')], errs=['67015 查重设置单例缺失(单例未落地时走出厂默认不报)'], resp='List(customerId/customerName/ownerName/customerStage/archiveStatus/masked)'), dict(dir='A4 客户管理/客户管理', name='信用代码查重', method='GET', path='/api/customer/check-credit-code', body='none', desc='L1 即时校验(失焦/企查查回填后调用);L3 保存时硬拦;无权命中只提示「系统已存在该企业」。', params=[('creditCode', 'string', 1, '统一社会信用代码(18 位)')], errs=['67003 已被占用'], resp='CreditCodeCheckDTO(exists 等布尔结构)'), dict(dir='A4 客户管理/客户管理', name='工商信息查询', method='GET', path='/api/customer/company-lookup', body='none', desc='企查查 seam(一期 stub 返回空数组);回填后立即调 check-credit-code。', params=[('companyName', 'string', 1, '公司名称关键字')], errs=[], resp='List(V1 stub 恒 [])'), # ============ A4 客户管理/客户三 workspace(3) ============ dict(dir='A4 客户管理/客户三 workspace', name='workspace 列表分页', method='POST', path='/api/customer/workspace/{workspace}/page', body='form', desc='workspace 三值路径段:mine(我的客户)/ overview(客户总览)/ pool(客户公海)。overview 走 DataScope(管理员可见全部),mine=负责人或协同人,pool=owner 空按部门可见范围。projectCount 恒 null 前端显 "--"(契约)。', params=[('viewType', 'string', 0, '内置视图:ASSIGNED 我负责的 / COLLABORATING 我协同的 / FOLLOW_UP_DUE 待跟进 / FOCUSED 我关注的 / RECENT(不接);空=基础集'), ('savedViewId', 'string', 0, '自定义视图 id(crm-preference;叠加检索条件不改 @DataScope 数据范围)'), ('archiveStatus', 'number', 0, '档案状态:1 有效 2 已归档;默认 1(D19 归档=删除语义默认排除)'), ('customerStage', 'number', 0, '客户阶段:1 潜在 2 重潜 3 已成交'), ('customerType', 'string', 0, '客户类型字典 code'), ('customerStarLevel', 'number', 0, '客户星级 1~5'), ('relationStarLevel', 'number', 0, '关系星级 1~5'), ('industryCode', 'string', 0, '行业一级 code'), ('provinceCode', 'string', 0, '省国标 code'), ('groupColumn', 'string', 0, '看板分组列(白名单:stage/star/relation,越界静默忽略)'), ('groupValue', 'number', 0, '看板分组取值(board/cards 与 groupColumn 配对)')] + BP, errs=['67002 客户不存在'], resp='PageResult(主表快照 + opportunityCount 真算 / projectCount 恒 null)'), dict(dir='A4 客户管理/客户三 workspace', name='看板分组汇总', method='POST', path='/api/customer/workspace/{workspace}/board/summary', body='form', desc='看板三分组数量汇总(D20);groupColumn 白名单 stage(客户阶段)/star(客户星级)/relation(关系星级);仅返回有数据分组,空列骨架前端补全。summary 与 page/cards 同一 WHERE 口径,数字自洽。', params=[('viewType', 'string', 0, '内置视图(同列表分页)'), ('groupColumn', 'string', 1, '分组列:stage/star/relation')] + BP, errs=[], resp='List([{groupValue,count}])'), dict(dir='A4 客户管理/客户三 workspace', name='看板单列卡片', method='POST', path='/api/customer/workspace/{workspace}/board/cards', body='form', desc='groupColumn + groupValue 取单列卡片,口径与列表/summary 一致;需分页参数。', params=[('groupColumn', 'string', 1, '分组列:stage/star/relation'), ('groupValue', 'number', 1, '分组取值(stage 1/2/3、星级 1~5)'), ('viewType', 'string', 0, '内置视图(同列表分页)')] + BP, errs=[], resp='PageResult'), # ============ A4 客户管理/归属流转(9) ============ dict(dir='A4 客户管理/归属流转', name='领取客户', method='POST', path='/api/customer/claim', body='none', desc='公海→当前用户;校验仍在公海 + 无进行中商机;D25 跟进锚点为空时赋当下。幂等口径见实现。', params=[('id', 'string', 1, '客户 id')], errs=['67002 客户不存在', '67004 状态不允许'], resp='null(Result,data=null 非失败)'), dict(dir='A4 客户管理/归属流转', name='批量领取', method='POST', path='/api/customer/claim-batch', body='none', desc='逐条独立事务,部分成功;失败明细按原因分类(BatchResult)。', params=[('ids', 'string', 1, '客户 id 集合(重复键 ids=...&ids=...)')], errs=['67002', '67004'], resp='BatchResult{successCount,failures:[{id,reason}]}'), dict(dir='A4 客户管理/归属流转', name='分配客户', method='POST', path='/api/customer/assign', body='none', desc='换销售负责人;目标人员启用在职 + 非原负责人;不校验进行中商机(issues-03 §E)。写 oplog TRANSFER(实测口径)。', params=[('id', 'string', 1, '客户 id'), ('userId', 'string', 1, '目标销售用户 id')], errs=['67002 客户不存在', '67012 分配非法'], resp='null'), dict(dir='A4 客户管理/归属流转', name='批量分配', method='POST', path='/api/customer/assign-batch', body='none', desc='目标人员统一校验一次(无效整批拒绝);其余逐条独立事务部分成功。', params=[('ids', 'string', 1, '客户 id 集合'), ('userId', 'string', 1, '目标销售用户 id')], errs=['67002', '67012'], resp='BatchResult{successCount,failures}'), dict(dir='A4 客户管理/归属流转', name='抛公海', method='POST', path='/api/customer/release-pool', body='none', desc='清 owner + 协同人(部门锚点保留);存在进行中商机则 67007 阻断。写 oplog POOL。', params=[('id', 'string', 1, '客户 id')], errs=['67002 客户不存在', '67007 公海阻断(进行中商机)'], resp='null'), dict(dir='A4 客户管理/归属流转', name='批量抛公海', method='POST', path='/api/customer/release-pool-batch', body='none', desc='逐条独立事务,部分成功;失败明细按原因分类。', params=[('ids', 'string', 1, '客户 id 集合')], errs=['67002', '67007'], resp='BatchResult{successCount,failures}'), dict(dir='A4 客户管理/归属流转', name='归档客户', method='POST', path='/api/customer/archive', body='none', desc='仅销售负责人;存在进行中商机则 67006 阻断;归档=删除语义(D19:列表默认排除,archiveStatus=2 可见)。', params=[('id', 'string', 1, '客户 id')], errs=['67002 客户不存在', '67004 状态不允许', '67006 归档阻断(进行中商机)'], resp='null'), dict(dir='A4 客户管理/归属流转', name='批量归档', method='POST', path='/api/customer/archive-batch', body='none', desc='整批预检:任一失败整批不执行(原型 A4-3-1 §3.6);预检过后逐条独立事务。', params=[('ids', 'string', 1, '客户 id 集合')], errs=['67002', '67004', '67006'], resp='BatchResult{successCount,failures}'), dict(dir='A4 客户管理/归属流转', name='恢复客户', method='POST', path='/api/customer/restore', body='none', desc='已归档→有效;仅销售负责人。写 oplog RESTORE。', params=[('id', 'string', 1, '客户 id')], errs=['67002 客户不存在', '67004 状态不允许'], resp='null'), # ============ A4 客户管理/客户详情页(8) ============ dict(dir='A4 客户管理/客户详情页', name='详情公共头部', method='GET', path='/api/customer/{id}/detail-head', body='none', desc='8 页签共享:阶段只读条/星级/协议 VIP 快照/负责人/最近跟进摘要/待跟进提醒/关联业务计数;汇总卡 5 字段(wonProjectAmount/planEstimateAmount/ongoingOpportunityAmount/contractAmount/paidAmount)恒 null 前端显 "--"(grill Q18 契约)。', params=[], errs=['67002 客户不存在'], resp='CustomerDetailHeadDTO(快照 + lastFollowSummary/lastFollowTime/nextFollowTime + opportunityCount/projectCount)'), dict(dir='A4 客户管理/客户详情页', name='团队成员页签', method='GET', path='/api/customer/{id}/members', body='none', desc='销售负责人(快照)+ 协同人列表;负责人 ROLE_OWNER 在前。', params=[], errs=['67002 客户不存在'], resp='List'), dict(dir='A4 客户管理/客户详情页', name='添加团队成员', method='POST', path='/api/customer/{id}/members', body='form', desc='原型 11.1 搜索多选;两阶段校验整批拒绝(名单空/含负责人/重复加入/用户停用 → 67016);批内幂等去重;姓名快照;oplog MEMBER_ADD。', params=[('memberUserIds', 'string', 1, '协同人用户 id 列表(重复键 memberUserIds=...&memberUserIds=...)')], errs=['67002 客户不存在', '67016 成员非法'], resp='null'), dict(dir='A4 客户管理/客户详情页', name='移除团队成员', method='DELETE', path='/api/customer/{id}/members/{memberUserId}', body='none', desc='原型 11.1 二次确认;软删保留历史(delete_key 复用键,可重复加入);oplog MEMBER_REMOVE。', params=[], errs=['67002 客户不存在', '67016 成员不存在'], resp='null'), dict(dir='A4 客户管理/客户详情页', name='关联商机页签', method='GET', path='/api/customer/{id}/opportunities', body='none', desc='只读反查 crm-opportunity(port seam,真源接线归票 11);port 失败前端显 "--" 不显 0。', params=BP, errs=['67002 客户不存在'], resp='PageResult(商机快照字段)'), dict(dir='A4 客户管理/客户详情页', name='操作日志分页', method='GET', path='/api/customer/{id}/oplog/page', body='none', desc='append-only 审计,无限期保留;时间倒序 + 关键字/时间范围过滤;detail 叙事「把 {字段} 从 {旧} 修改为 {新}」(D13),阶段变化 operator=系统。', params=[('startTime', 'string', 0, '操作时间范围起(含)yyyy-MM-dd HH:mm:ss'), ('endTime', 'string', 0, '操作时间范围止(含)'), ('action', 'string', 0, '操作类型筛选(11 种白名单:CREATE/UPDATE/FOLLOW/ARCHIVE/RESTORE/TRANSFER/CLAIM/POOL/STAGE_CHANGE/MEMBER_ADD/MEMBER_REMOVE)')] + BP, errs=['67002 客户不存在'], resp='PageResult'), dict(dir='A4 客户管理/客户详情页', name='新增跟进', method='POST', path='/api/customer/{id}/follow', body='form', desc='append-only 提交后不可改删(纠错=新增补充);防重复提交;写库刷新超期提醒锚点 last_valid_follow_time(旧未发提醒自动置已失效)。已知缺陷 D-02(P2):nextFollowTime 早于当前未拦截(spec 要求 67009)。', params=[('followWay', 'string', 1, '跟进方式字典 code(follow_way:电话沟通/上门拜访/微信对接/线上会议/展会沟通)'), ('followContent', 'string', 1, '跟进内容(空 → 67009)'), ('nextFollowTime', 'string', 0, '下次跟进时间(ISO T 分隔 yyyy-MM-ddTHH:mm:ss;空格分隔报 400;有值进「待跟进」筛选)')], errs=['67002 客户不存在', '67009 跟进入参非法'], resp='新跟进记录 id(string)'), dict(dir='A4 客户管理/客户详情页', name='跟进记录分页', method='GET', path='/api/customer/{id}/follow/page', body='none', desc='时间倒序 + 日期分组(前端渲染);按 follow_way/关键字/时间范围过滤。', params=[('followWay', 'string', 0, '跟进方式字典 code(精确)'), ('startTime', 'string', 0, '跟进时间范围起(含)'), ('endTime', 'string', 0, '跟进时间范围止(含)')] + BP, errs=['67002 客户不存在'], resp='PageResult(followWay/followContent/nextFollowTime…)'), # ============ A4 客户管理/A4-5 联系人(7) ============ dict(dir='A4 客户管理/A4-5 联系人', name='新增联系人', method='POST', path='/api/customer/contact', body='form', desc='所属客户必填且对当前用户可见;联系电话手机号格式。权限继承所属客户(无独立 ACL),无权 67008/67002。', params=[('customerId', 'string', 1, '所属客户 id(新增必填;编辑时以库内归属为准不可换客户)'), ('name', 'string', 1, '联系人姓名'), ('jobTitleName', 'string', 1, '职务名称(文本;自定义职务时以下三列空)'), ('jobTitleId', 'string', 0, '标准职务 id'), ('jobTitleCategory', 'string', 0, '职务类别(高层管理/采购/…)'), ('jobTitleLevel', 'number', 0, '职务层级'), ('phone', 'string', 0, '联系电话(手机号格式)'), ('isKeyContact', 'number', 0, '是否关键联系人:1是 0否'), ('isInternal', 'number', 0, '是否内线:1是 0否'), ('giftRemark', 'string', 0, '礼品备注'), ('source', 'string', 0, '来源字典 contact_source:组会/组局/转介绍/其他')], errs=['67008 联系人入参非法/不属于该客户', '67002 客户不存在'], resp='新联系人 id(string)'), dict(dir='A4 客户管理/A4-5 联系人', name='编辑联系人', method='PUT', path='/api/customer/contact/{id}', body='form', desc='归属客户不可换;联系电话手机号格式。字段同新增(customerId 以库内归属为准)。', params=[('name', 'string', 1, '联系人姓名'), ('jobTitleName', 'string', 1, '职务名称'), ('phone', 'string', 0, '联系电话(手机号格式)'), ('isKeyContact', 'number', 0, '是否关键联系人'), ('isInternal', 'number', 0, '是否内线'), ('giftRemark', 'string', 0, '礼品备注'), ('source', 'string', 0, '来源字典 contact_source')], errs=['67008 联系人入参非法'], resp='null'), dict(dir='A4 客户管理/A4-5 联系人', name='删除联系人', method='DELETE', path='/api/customer/contact/{id}', body='none', desc='软删(可能被商机/项目引用做快照)。', params=[], errs=['67008 联系人不存在'], resp='null'), dict(dir='A4 客户管理/A4-5 联系人', name='联系人详情', method='GET', path='/api/customer/contact/{id}', body='none', desc='带出所属公司名(customerName/customerNo)。', params=[], errs=['67008 联系人不存在'], resp='ContactDTO(含 customerName/customerNo 仅出参字段)'), dict(dir='A4 客户管理/A4-5 联系人', name='顶级联系人列表', method='GET', path='/api/customer/contact/page', body='none', desc='A4-5-1 跨客户分页;keyword 模糊姓名/电话/客户名;客户可见性收口。', params=BP, errs=[], resp='PageResult'), dict(dir='A4 客户管理/A4-5 联系人', name='联系人全局搜索', method='GET', path='/api/customer/contact/search', body='none', desc='商机「商机关键联系人」数据源:姓名/电话模糊,带出所属公司名。', params=[('keyword', 'string', 1, '关键字(姓名/电话模糊)'), ('limit', 'number', 0, '条数上限,默认 20')], errs=[], resp='List'), dict(dir='A4 客户管理/A4-5 联系人', name='电话软提示', method='GET', path='/api/customer/contact/check-phone', body='none', desc='D26:跨客户电话命中只提示疑似重复、不阻断;受查重设置「联系电话」开关控制(单例未落地=出厂默认电话软提示)。', params=[('phone', 'string', 1, '联系人手机号'), ('excludeId', 'string', 0, '排除的联系人 id(编辑页传自身)')], errs=[], resp='List(跨客户命中列表)'), # ============ A4 客户管理/客户关注与重点标记(5) ============ dict(dir='A4 客户管理/客户关注与重点标记', name='关注客户', method='POST', path='/api/customer/{id}/focus', body='none', desc='幂等(重复关注直接成功);行存在性=关注态;不写 oplog。', params=[], errs=['67002 客户不存在'], resp='null'), dict(dir='A4 客户管理/客户关注与重点标记', name='取消关注客户', method='POST', path='/api/customer/{id}/unfocus', body='none', desc='物理删行,连带清除重点标记;未关注时幂等成功。', params=[], errs=[], resp='null'), dict(dir='A4 客户管理/客户关注与重点标记', name='标记重点客户', method='POST', path='/api/customer/{id}/star', body='none', desc='详情页操作区按钮;自动确保关注行存在(未关注先建行 starred=1);幂等;不写 oplog。', params=[], errs=['67002 客户不存在'], resp='null'), dict(dir='A4 客户管理/客户关注与重点标记', name='取消重点标记', method='POST', path='/api/customer/{id}/unstar', body='none', desc='保留关注行,仅置 starred=0;幂等。', params=[], errs=[], resp='null'), dict(dir='A4 客户管理/客户关注与重点标记', name='批量关注客户', method='POST', path='/api/customer/focus-batch', body='none', desc='对称商机 focus-batch;逐个幂等关注。', params=[('ids', 'string', 1, '客户 id 集合(重复键)')], errs=['67002 客户不存在'], resp='null'), # ============ A4 客户管理/客户交割(6) ============ dict(dir='A4 客户管理/客户交割', name='发起前客户预览', method='GET', path='/api/customer/transfer/preview', body='none', desc='当前用户名下可交接客户(唯一负责人 + 有效 + 非归档 + 非公海);initiate 无 customerIds 入参,圈定=预览全量。', params=[], errs=[], resp='List(customerId/customerName/customerNo/customerStarLevel/customerStage)'), dict(dir='A4 客户管理/客户交割', name='发起交接', method='POST', path='/api/customer/transfer/initiate', body='form', desc='整批原子:名下客户 owner→接收总监 + 同步商机/项目 owner,任一失败全部回滚不生成交接单;存在未完成交接单 67010 阻断。reason=resign 时接收总监按部门 leader 自动推导(D22);交接编号 JG+yyyyMMdd+4 位;oplog TRANSFER(系统操作,含交接编号)。', params=[('reason', 'string', 1, '交接原因字典 code(transfer_reason:resign 员工离职 / transfer_post 岗位调动 / region_adjust 区域调整)'), ('toDirectorId', 'string', 0, '接收总监用户 id(resign 自动推导不传;调动/区域调整必传且校验在范围内)'), ('remark', 'string', 0, '备注(选填)')], errs=['67010 存在未完成交接单', '67011 无可交接客户/接收总监不合法/原因缺失', '67012 分配非法'], resp='TransferDetailDTO(交接单头:transferNo/status/totalCount/assignedCount…)'), dict(dir='A4 客户管理/客户交割', name='交接详情', method='GET', path='/api/customer/transfer/{id}', body='none', desc='单头 + 明细(客户名实时回显,进度 = assignedCount/totalCount)。', params=[], errs=['67002 不存在'], resp='TransferDetailDTO(含明细 items:客户/原负责人/分配状态)'), dict(dir='A4 客户管理/客户交割', name='交接记录分页', method='GET', path='/api/customer/transfer/page', body='none', desc='交接编号/原因/状态/发起人关键字过滤,时间倒序。', params=[('transferNo', 'string', 0, '交接编号(模糊)'), ('reason', 'string', 0, '交接原因字典 code(精确)'), ('status', 'number', 0, '状态:0 待分配 1 已完成')] + BP, errs=[], resp='PageResult'), dict(dir='A4 客户管理/客户交割', name='客户分配', method='POST', path='/api/customer/transfer/{id}/assign', body='none', desc='接收总监→具体销售;逐条独立事务部分成功,失败明细列原因(67012:已分配/越权/为发起人本人/为当前负责人);全部分配完成自动置已完成(D23)。', params=[('customerIds', 'string', 1, '客户 id 集合(重复键)'), ('assignUserId', 'string', 1, '目标销售用户 id')], errs=['67012 分配非法'], resp='BatchResult{successCount,failures}'), dict(dir='A4 客户管理/客户交割', name='可分配销售列表', method='GET', path='/api/customer/transfer/{id}/assignable', body='none', desc='接收总监部门(含子部门)+ 启用在职;排除发起人与总监本人。已知缺陷 D-07(P1):该接口稳定超时(E2E 实测 60s×2 ReadTimeout,交割明细分配流程被阻断,缺陷验证模式登记)。', params=[], errs=[], resp='List(userId/userName/deptName;当前因 D-07 不可达)'), # ============ A4 客户管理/客户导入(6) ============ dict(dir='A4 客户管理/客户导入', name='上传导入文件并预校验', method='POST', path='/api/customer/import/upload', body='multipart', desc='文件守门(.xls/.xlsx、≤20MB、模板版本、单 sheet ≤5 万行)→ 行级判定(与执行同口径)→ 任务落待确认态(DRAFT),返回预计数 + 明细(上限 100 条)。已知缺陷 D-05(P1):is_biz_negotiated NOT NULL 无默认值且 createCustomer 未赋值——含 INSERT 行的导入必 FAILED(行不落库);D-04(P2):行异常文本超长致失败明细写不进 fail 表(0 行)。', params=[('file', 'file', 1, 'Excel 文件(.xlsx,multipart)'), ('importMode', 'string', 1, '导入方式:APPEND_ONLY 仅新增 / UPDATE_ONLY 仅更新 / UPSERT 有则更新')], errs=['67013 导入预校验失败(文件/表头/行数/模板版本)'], resp='ImportPreviewDTO(taskId + totalCount/insertCount/updateCount/failCount/suspectCount + 行级明细)'), dict(dir='A4 客户管理/客户导入', name='确认执行导入', method='POST', path='/api/customer/import/{taskId}/confirm', body='none', desc='待确认(DRAFT)→ 处理中(RUNNING)+ 触发异步执行;重复确认 67014 阻断(两段式)。', params=[], errs=['67014 状态机不允许'], resp='任务 taskId(string)'), dict(dir='A4 客户管理/客户导入', name='任务结果', method='GET', path='/api/customer/import/{taskId}', body='none', desc='结果页:状态 + 总行数/新增/更新/未变化/失败/疑似重复 + 提交人/时间。状态机 0 DRAFT→1 RUNNING→2 DONE/3 FAILED。', params=[], errs=['67002 任务不存在'], resp='ImportResultDTO'), dict(dir='A4 客户管理/客户导入', name='失败疑似重复明细', method='GET', path='/api/customer/import/{taskId}/failures', body='none', desc='sheet/行号/客户识别/失败字段/原因;疑似重复行以「疑似重复」前缀区分。已知缺陷 D-04(P2):行异常文本超 varchar(500) 时明细写不进(实测 0 行)。', params=[], errs=['67014 状态机不允许'], resp='List'), dict(dir='A4 客户管理/客户导入', name='最近导入记录', method='GET', path='/api/customer/import/page', body='none', desc='仅当前用户任务(状态/方式过滤),时间倒序。', params=[('status', 'number', 0, '状态:0 待确认 1 处理中 2 已完成 3 已失败'), ('importMode', 'string', 0, '导入方式:APPEND_ONLY/UPDATE_ONLY/UPSERT')] + BP, errs=[], resp='PageResult'), dict(dir='A4 客户管理/客户导入', name='下载导入模板', method='GET', path='/api/customer/import/template', body='none', desc='客户 + 联系人双 sheet 纯表头(表头即模板版本锚点),无示例行。RFC 5987 中文文件名。', params=[], errs=[], resp='非信封:二进制文件流(Content-Disposition: attachment; filename*=UTF-8\'\'…xlsx);失败时标准 JSON 信封'), # ============ A4 客户管理/模块健康(1) ============ dict(dir='A4 客户管理/模块健康', name='健康探针', method='GET', path='/api/customer/ping', body='none', desc='模块骨架探针(联通性,无业务)。', params=[], errs=[], resp='文字说明:标准 JSON 信封,data 为固定探针文案'), # ============ 客户管理/超期提醒规则(crm-rule,tag 原文根级)(2) ============ dict(dir='客户管理/超期提醒规则', name='当前提醒规则', method='GET', path='/api/rule/customer/reminder', body='none', desc='超期提醒单例配置(A7-3-3-1,crm-rule 跨模块服务于客户域):总开关/首次触发 30 天/二次间隔 7 天/独立开关。Job 每轮只补「下一条未建到期提醒」(升级提醒链语义,E2E 实测 remindSeq 连续递增)。', params=[], errs=[], resp='CustomerReminderRule{masterEnabled,firstTriggerEnabled,firstTriggerDays,secondIntervalEnabled,secondIntervalDays}(Integer 0/1 与天数)'), dict(dir='客户管理/超期提醒规则', name='保存提醒规则', method='PUT', path='/api/rule/customer/reminder', body='json', desc='全量覆盖保存(唯一 @RequestBody JSON 端点):开关字段缺省按 0(禁用)处理,不静默保留旧值;天数非正整数报 64023 且不落库。保存即生效,Job 下一轮按新配置执行。', params=[('masterEnabled', 'number', 0, '总开关 1 启用 0 停用(缺省=0)'), ('firstTriggerEnabled', 'number', 0, '首次触发开关(缺省=0)'), ('firstTriggerDays', 'number', 0, '首次触发天数(正整数,非正报 64023)'), ('secondIntervalEnabled', 'number', 0, '二次提醒开关(缺省=0)'), ('secondIntervalDays', 'number', 0, '二次提醒间隔天数(正整数,非正报 64023)')], errs=['64023 天数非正整数(校验失败不落库)'], resp='CustomerReminderRule(保存后全量回显)'), ] # 路径占位 → 示例值 PATH_VARS = { '{id}': CID, '{customerId}': CID, '{memberUserId}': BUDDY, '{taskId}': 'TASKID', '{workspace}': 'mine', } # 语义示例值兜底(params 无实测时按字段名取) SEMANTIC = { 'customerName': 'e2c-恒信达科技有限公司', 'customerType': 'customer_type_01', 'provinceCode': '440000', 'cityCode': '440100', 'districtCode': '440103', 'industryCode': 'gov', 'customerStarLevel': '3', 'relationStarLevel': '3', 'ownerUserId': ADMIN, 'userId': BUDDY, 'toDirectorId': ADMIN, 'followWay': 'follow_way_01', 'followContent': 'e2c-F08 跟进实测:电话沟通年度合作意向', 'nextFollowTime': '2026-09-10T10:00:00', 'keyword': '恒信达', 'name': 'e2c-恒信达科技有限公司', 'creditCode': '91440101MA9ABC1234', 'companyName': '恒信达', 'phone': '13800138000', 'reason': 'resign', 'remark': 'e2e 实测备注', 'importMode': 'APPEND_ONLY', 'viewType': 'ASSIGNED', 'savedViewId': '', 'groupColumn': 'stage', 'groupValue': '1', 'archiveStatus': '1', 'customerStage': '1', 'action': 'CREATE', 'startTime': '2026-09-01 00:00:00', 'endTime': '2026-09-30 23:59:59', 'ids': CID_POOL, 'memberUserIds': BUDDY, 'customerIds': CID_TRANSFER, 'assignUserId': BUDDY, 'excludeId': '', 'limit': '20', 'unifiedCreditCode': '91440101MA9ABC1234', 'current': '1', 'size': '10', 'orderBy': '', 'transferNo': 'JG202609030001', 'status': '2', 'confirmSimilar': '', 'source': 'contact_source_01', }