{
  "openapi": "3.0.3",
  "info": {
    "title": "PushMsg CRM 微信推送接口",
    "version": "1.1.0",
    "description": "每个调用系统必须同时提交 API Key 和 API Secret；接收人按系统、公众号通道和 OpenID 白名单隔离，默认无权限。旧版单 Key 须由管理员轮换凭据并配置接收人。markedAt 按 Asia/Shanghai 解释。SENT 表示微信接受，不代表用户已收到或阅读。",
    "contact": {
      "name": "PushMsg 接口维护方"
    }
  },
  "servers": [
    {
      "url": "https://openid.xianboss.com",
      "description": "生产环境，可信 HTTPS"
    }
  ],
  "tags": [
    {
      "name": "CRM",
      "description": "新客户跟进通知提交、状态查询和当前模板配置。"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": [],
      "ApiSecretAuth": []
    },
    {
      "BearerAuth": [],
      "ApiSecretAuth": []
    }
  ],
  "paths": {
    "/api/v1/crm/push": {
      "post": {
        "tags": [
          "CRM"
        ],
        "operationId": "pushCrmCustomerFollowup",
        "summary": "提交 CRM 新客户跟进通知",
        "description": "同一调用方+requestId 用于幂等去重。同内容重复返回原任务，内容变化返回409。CRM不传channelId/templateId/type/data；额外字段返回400。网络重试复用原requestId和原内容。",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CrmPushRequest"
              },
              "example": {
                "requestId": "crm-customer-C0001-followup-20261026-001",
                "openId": "YOUR_OPENID",
                "customerName": "测试客户",
                "customerType": "潜在客户",
                "customerSource": "CRM录入",
                "customerPhone": "13800000000",
                "markedAt": "2026-10-26 11:00",
                "url": "https://crm.example.com/customers/C0001"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "新请求已持久化入队，尚不表示微信已接受。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Receipt"
                }
              }
            }
          },
          "200": {
            "description": "幂等重复请求，返回原消息 ID、当前状态和 duplicate=true，不创建新任务。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Receipt"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/crm/messages/{id}": {
      "get": {
        "tags": [
          "CRM"
        ],
        "operationId": "getCrmMessageStatus",
        "summary": "查询当前调用方的消息状态",
        "description": "仅查询当前调用方在CRM配置通道下的消息，不限定必须通过CRM包装接口创建。UNKNOWN须人工核实，不要自动另建任务。",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "提交时返回的消息UUID。",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "当前调用方在当前 CRM 通道下的消息状态。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/crm/template": {
      "get": {
        "tags": [
          "CRM"
        ],
        "operationId": "getCrmTemplate",
        "summary": "查看当前 CRM 模板配置",
        "description": "需要当前CRM通道权限。返回服务端配置，提交时无需回传这些微信字段。",
        "responses": {
          "200": {
            "description": "当前配置的 CRM 模板及业务字段映射，不返回公众号凭据。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateInfo"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "分配给当前调用系统的 API Key，必须同时提供 X-API-Secret。"
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "可替代 X-API-Key 提供当前系统的 Key；X-API-Secret 仍必填。"
      },
      "ApiSecretAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Secret",
        "description": "与当前 API Key 配对的 Secret；创建或轮换时仅展示一次，保存于业务系统服务端。"
      }
    },
    "schemas": {
      "CrmPushRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "requestId",
          "openId",
          "customerName",
          "customerType",
          "customerSource",
          "customerPhone",
          "markedAt"
        ],
        "properties": {
          "requestId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "同一业务调用方内唯一的请求标识；网络重试保持原值和相同内容。",
            "example": "crm-customer-C0001-followup-20261026-001"
          },
          "openId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "通知接收人（如负责跟进的 CRM 员工）在目标公众号下的 OpenID，不是 customerName 所指的客户姓名。",
            "example": "YOUR_OPENID"
          },
          "customerName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 20,
            "pattern": "^(?!\\s*$)[^\\u0000-\\u001F\\u007F-\\u009F]+$",
            "description": "CRM 客户记录中的客户名称，不代表通知接收人。",
            "example": "测试客户"
          },
          "customerType": {
            "type": "string",
            "minLength": 1,
            "maxLength": 20,
            "pattern": "^(?!\\s*$)[^\\u0000-\\u001F\\u007F-\\u009F]+$",
            "description": "客户类型。",
            "example": "潜在客户"
          },
          "customerSource": {
            "type": "string",
            "minLength": 1,
            "maxLength": 20,
            "pattern": "^(?!\\s*$)[^\\u0000-\\u001F\\u007F-\\u009F]+$",
            "description": "客户来源。",
            "example": "CRM录入"
          },
          "customerPhone": {
            "type": "string",
            "minLength": 3,
            "maxLength": 17,
            "pattern": "^(?=.*[0-9])[0-9+() -]{3,17}$",
            "description": "客户电话，至少包含一个数字，只允许数字、+、括号、空格、短横线。",
            "example": "13800000000"
          },
          "markedAt": {
            "type": "string",
            "minLength": 16,
            "maxLength": 16,
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$",
            "description": "有效的北京时间，严格 yyyy-MM-dd HH:mm。不自动生成时间，不接受偏移量、秒或无效日历日期。",
            "example": "2026-10-26 11:00"
          },
          "url": {
            "type": "string",
            "nullable": true,
            "maxLength": 2048,
            "pattern": "^https?://[^/\\s?#]+[^\\s]*$",
            "description": "可选的 CRM 详情完整 HTTP/HTTPS URL；可省略或传 null。",
            "example": "https://crm.example.com/customers/C0001"
          }
        },
        "example": {
          "requestId": "crm-customer-C0001-followup-20261026-001",
          "openId": "YOUR_OPENID",
          "customerName": "测试客户",
          "customerType": "潜在客户",
          "customerSource": "CRM录入",
          "customerPhone": "13800000000",
          "markedAt": "2026-10-26 11:00",
          "url": "https://crm.example.com/customers/C0001"
        },
        "description": "只接受列出的业务字段，所有非 null 字段必须是字符串且不得包含控制字符。"
      },
      "Receipt": {
        "type": "object",
        "required": [
          "id",
          "status",
          "duplicate"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "PROCESSING",
              "RETRY",
              "SENT",
              "FAILED",
              "UNKNOWN"
            ]
          },
          "duplicate": {
            "type": "boolean"
          }
        },
        "example": {
          "id": "11111111-2222-4333-8444-555555555555",
          "status": "PENDING",
          "duplicate": false
        }
      },
      "MessageStatus": {
        "type": "object",
        "required": [
          "id",
          "requestId",
          "status",
          "attempts",
          "maxAttempts",
          "wechatMessageId",
          "lastError",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "requestId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "PROCESSING",
              "RETRY",
              "SENT",
              "FAILED",
              "UNKNOWN"
            ],
            "description": "SENT 仅表示微信接口接受；UNKNOWN 须人工核实，不要盲目重发。"
          },
          "attempts": {
            "type": "integer",
            "minimum": 0
          },
          "maxAttempts": {
            "type": "integer",
            "minimum": 1
          },
          "wechatMessageId": {
            "type": "string",
            "nullable": true,
            "description": "以字符串处理微信消息 ID，避免数值精度损失。"
          },
          "lastError": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "北京时间的本地日期时间，可能带小数秒，不含时区后缀。",
            "example": "2026-10-26T11:00:00"
          },
          "updatedAt": {
            "type": "string",
            "description": "北京时间的本地日期时间，可能带小数秒，不含时区后缀。",
            "example": "2026-10-26T11:00:01"
          }
        }
      },
      "TemplateField": {
        "type": "object",
        "required": [
          "field",
          "templateField",
          "label"
        ],
        "properties": {
          "field": {
            "type": "string"
          },
          "templateField": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        }
      },
      "TemplateInfo": {
        "type": "object",
        "required": [
          "channelId",
          "templateName",
          "templateId",
          "fields",
          "timeZone"
        ],
        "properties": {
          "channelId": {
            "type": "string"
          },
          "templateName": {
            "type": "string"
          },
          "templateId": {
            "type": "string"
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateField"
            }
          },
          "timeZone": {
            "type": "string"
          }
        },
        "example": {
          "channelId": "xianbao",
          "templateName": "新客户跟进通知",
          "templateId": "OpH0RvriM-Zi5WmN25X6ox0FY6rKMoStoXb_ab-jiRw",
          "fields": [
            {
              "field": "customerName",
              "templateField": "thing2",
              "label": "客户名称"
            },
            {
              "field": "customerType",
              "templateField": "thing3",
              "label": "客户类型"
            },
            {
              "field": "customerSource",
              "templateField": "thing5",
              "label": "客户来源"
            },
            {
              "field": "customerPhone",
              "templateField": "phone_number6",
              "label": "客户电话"
            },
            {
              "field": "markedAt",
              "templateField": "time8",
              "label": "标记时间"
            }
          ],
          "timeZone": "Asia/Shanghai"
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "example": {
          "code": "IDEMPOTENCY_CONFLICT",
          "message": "同一 requestId 已用于不同消息，请为新消息生成新 requestId"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "参数无效：VALIDATION_ERROR、INVALID_REQUEST、INVALID_CRM_REQUEST、INVALID_CRM_TIME、INVALID_CRM_PHONE、UNSUPPORTED_CRM_FIELD、INVALID_URL 或 CHANNEL_DISABLED。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Key 或 Secret 缺失、不匹配、尚未配置，或调用系统已停用。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "CHANNEL_FORBIDDEN：无通道权限；RECIPIENT_FORBIDDEN：OpenID 未绑定到本系统及通道、已停用或删除。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "消息不存在、不属于当前调用方，或无通道读取权限。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "IDEMPOTENCY_CONFLICT：同一调用方的同一 requestId 对应不同消息内容。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooLarge": {
        "description": "请求体超过入口或应用的 64KB 上限；入口可能返回 HTML 错误页，请按 HTTP 状态处理。"
      },
      "RateLimited": {
        "description": "来源 IP 限流，延迟后复用原 requestId 和原内容重试。入口可能返回 HTML 错误页。"
      },
      "ServerError": {
        "description": "服务内部错误，保留请求标识，稍后查询或重试。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unavailable": {
        "description": "DATABASE_UNAVAILABLE 或 CRM_NOT_CONFIGURED，数据库或当前 CRM 配置不可用。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
