## 接入地址与凭证

接口地址：`https://www.350c.com/api/v1`，版本 v1。下表中的路径均相对于此地址，不要重复拼接 `/api/v1`。正式环境使用此 HTTPS 地址，推荐统一从主站调用；发布城市由 `city_id` 指定，不需要分别创建各分站的密钥。本地 hosts 只对本机有效，第三方电脑无法直接连接你的本地开发站点。不要把本机开发服务直接暴露到公网。

在会员中心的“API 密钥”创建凭证。请求头使用 `Authorization: Bearer <完整密钥>`，不要放到 URL、前端代码或公开仓库。每个账号只能保留一个密钥，可选择 30/90/365 天有效期。刷新后默认隐藏完整密钥；点击眼睛，验证当前密码后即可显示及复制，再次点击眼睛隐藏。查看满一分钟或页面进入后台会自动隐藏，每次重新显示都需验证密码。

新密钥额外加密保存以支持查看，API 认证仍使用哈希。旧版只有哈希的密钥无法还原，页面会提示重置一次；不会自动作废旧密钥。过期或需要更换时展开“重置密钥”，验证当前密码并确认作废旧密钥，生成后更新调用程序。重置后旧密钥立即失效。也可以先撤销再创建。

修改/重置密码、停用账号会使旧密钥失效，恢复账号也不会恢复旧密钥；主动撤销立即生效。失效密钥仍占一个名额，需重置或撤销后再创建。

API 只接受 Bearer 密钥，不接受网页登录 Cookie。密钥可查询分类、本人已审核专栏、自有信息状态和本人上传的图片，以及上传图片和提交信息；即使属于管理员也不能审核、支付或读取他人私有信息。退出网页登录不撤销 API 密钥。生产环境只接受 HTTPS。本接口面向服务器/脚本调用，不开放第三方网页跨域调用。

## 先验证连通性

会员中心“API 密钥”页提供“检测连接”，由平台服务器使用当前密钥向固定主站 `/quota` 发起 HTTPS GET，不发布文章、不消耗额度、不重置密钥。每个会员每分钟最多检测 3 次；不会把密钥或接口响应正文显示在检测结果中。此检测确认平台服务器到接口的连通性，第三方调用服务器的网络仍需用下方命令检查。

“调用记录”仅显示本人已认证请求的接口路径、HTTP 状态、耗时和时间，保留最近 30 天。失败说明按 HTTP 状态提供排查方向，具体字段错误以调用方收到的 JSON 响应为准；未认证的请求无法归属账号，不显示在会员记录中。密钥到期前 7 天、前 1 天及到期后会通过消息中心提醒，每个阶段只提醒一次。后台停用 API 不会恢复或延长密钥有效期。

在调用程序的服务器中配置 `LINJI_API_KEY` 环境变量，值必须是会员中心复制的完整密钥，不是页面显示的编号，也不是账号密码。新申请或重置的密钥格式为 `key_` 加 64 位随机十六进制字符串，不含编号和竖线。已生成的 `编号|linji_…` 旧格式继续有效，无需主动重置；使用旧格式时必须保留完整编号和竖线。不要手动修改密钥前缀，也不要在日志中输出它。

```bash
curl --silent --show-error --connect-timeout 10 --max-time 30 \
  'https://www.350c.com/api/v1/quota' \
  -H "Authorization: Bearer $LINJI_API_KEY" \
  -H 'Accept: application/json'
```

HTTP 200 且返回 `data.daily_limit` 等余额字段表示密钥认证及接口连通正常；此请求不会发布文章或消耗额度。也可查询 `/catalog` 验证城市与分类。不要开启 `curl -v` 或记录 Authorization 请求头，以免泄露密钥。

HTTP 401：检查是否复制了完整密钥、是否过期/撤销，以及代理是否转发 Authorization。HTTP 403：检查后台是否停用了账号 API、账号是否停用或是否修改过密码，并确认使用 HTTPS。重置前先核对原因，重置会立即作废旧密钥。HTTP 429：按 `Retry-After` 等待，不要反复创建密钥。网络超时或证书错误属于连接问题，不表示密钥无效；不要通过关闭 TLS 校验解决证书问题。

## 接口

| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET | `/catalog` | 查询城市、大类、子类、细分类别和动态字段规则 |
| POST | `/listings` | 提交纯文本或富文本图文信息，可设置封面和自己的专栏，进入人工审核 |
| GET | `/listings/{id}` | 查询自己的信息状态及退回原因 |
| GET | `/quota` | 查询本人今日额度、发帖包余额及刷新券 |
| GET | `/columns` | 分页查询本人已审核公开的专栏，供 column_id 选择 |
| POST | `/media` | 上传一张正文或封面图片，返回可引用的 src |
| GET | `/media/{id}` | 携带密钥预览本人上传图片，返回 JPEG 文件而非 JSON |

请求携带 `Accept: application/json`。发布必须使用 `Content-Type: application/json`，整个 JSON 请求上限 64KB。富文本使用结构化 `body_document` 对象，不接受直接提交 HTML。正文图片和封面只能引用 `/media` 接口返回的 `src`，不能传任意图片 URL、Base64、COS 签名地址或别人的图片。只有图片上传接口接受 multipart。

这是会员分类信息接口，不是后台编辑部的 `/articles` 内容接口。目前没有 API 编辑、删除、创建/编辑/审核专栏接口；不要使用网页表单地址代替 API。即使网站开启了自动审核发布，API 新提交也需人工审核。`description` 是纯文本正文；提交 `body_document` 时由服务端提取纯文本用于审核、搜索和摘要，此时可不传 `description`，同时传入也以富文本为准。选填 `summary` 为摘要，未填时自动截取正文。

网页单张图片不超过 4MB，单次新上传图片合计不超过 40MB；已保存图片不计入本次上传容量。图片和封面选填，没有图片也可发布。

网页和 API、所有城市共用同一账号的发布额度。普通会员每天免费 10 条；已购发布套餐的每日额度与免费额度取较高值，不叠加，每天北京时间零点重置，未使用的不累计。每日额度用完后自动扣除发帖包 1 条，调用前应确认账号余额。首次成功提交审核扣一次，相同 external_id 和相同内容重试不重复扣；校验失败不扣。额度不足返回 422，`errors.quota` 给出原因。编辑、重审不重复扣；驳回、下架、删除不自动返还额度。

发布套餐有效期以购买时的商品及订单快照为准，赠送刷新券同时到期；发帖包长期有效。`GET /quota` 返回 `data.daily_limit`、`daily_remaining`、`pack_remaining`、`refresh_coupons`、`plan_expires_at`、`next_source`（free/daily/pack/null）和 `image_limit`，余额为查询时快照。网页正文与封面图片合计最多 20 张，同一图片不重复计数，不再按套餐区分。按天套餐可在会员中心补差价升级，到期时间不变、当天已用额度不重置；已有刷新券与按剩余时间折算的新增券一并转入。购买、升级、刷新操作在会员中心完成，不接受 API 支付或自动免审。

## 获取分类与城市

```bash
curl 'https://www.350c.com/api/v1/catalog' \
  -H "Authorization: Bearer $LINJI_API_KEY" \
  -H 'Accept: application/json'
```

返回 `data.cities`、`data.districts` 和 `data.categories`。城市使用真实 `id`，不要把区划 `code` 当作 ID。`data.categories` 是数组，大类的 `subcategories` 是以子类键为键的对象，每个值包含 `name`，并可包含 `types`（细分类别键与中文名称的映射），不是直接映射到中文字符串。例如 `subcategories.housekeeping.name` 是“家政”，`subcategories.housekeeping.types.hourly` 是“钟点工”。`fields` 给出该大类的附加字段（类型、必填、选项及数值范围）。`data.districts[city_id]` 给出该城市的区县及镇街选项。不要硬编码不同环境的城市/分类 ID；后台可修改分类，请始终以实际响应为准。

## 发布字段

| 字段 | 要求 |
| --- | --- |
| external_id | 必填，调用方的唯一文章编号，1–80 位；首位字母/数字，其余允许字母、数字、点、下划线、冒号、短横线 |
| city_id | 必填，城市 ID；即使调用城市子域名也以此字段为准 |
| district_code | 可选，区县/镇街编码字符串，须来自 catalog 的 data.districts[city_id]；传入时 area 由服务端写为标准区县名，null 表示全市。省略时仍兼容原 area 文本 |
| category_id | 必填，大类 ID |
| subcategory | 必填，属于该大类的子类键，如 services 下的 cleaning |
| subcategory_type | 子类有 types 时必填，必须来自该子类；没有 types 时不传 |
| title | 必填，6–80 字符 |
| description | 纯文本发布必填，20–5000 字符；提交 body_document 时可省略 |
| body_document | 可选，网页编辑器同格式的 JSON 对象；不是 HTML，也不是 JSON 字符串。提取后的正文须为 20–5000 字符 |
| summary | 可选，最多 160 字符；省略、null 或空字符串时自动截取正文。HTML 标签会被剥离，建议直接提交纯文本 |
| cover_mode | 可选，none（默认，无封面）、single（1 张）、triple（3 张） |
| cover_images | 可选，src 字符串数组；数量须与 cover_mode 一致，最多 3 张且不能重复；须为本人未过期、尚未绑定其他文章的上传图片 |
| column_id | 可选，本人已审核公开专栏的 ID，来自 GET /columns；每篇文章只允许一个专栏 |
| contact_name | 可省略，使用会员用户资料中的联系人；显式传入时最多 40 字符 |
| contact_phone | 可省略，使用会员用户资料中的联系电话；显式传入时须为 11 位大陆手机号，以 1 开头、第二位 3–9 |
| business_id | 可省略或为 null，按个人发布；商家发布须传本人已审核公开的商家编号，city_id 必须位于其服务范围内 |
| area | 可选，最多 80 字符，区县/街道描述 |
| attributes | 大类附加字段对象，按 catalog 的 fields 填写 |

不接受 user_id、status、published_at、promoted_until、intent、images 等额外顶层字段。归属会员和审核状态由服务端确定。原网页校验同样适用，包括动态字段、子类关系及必填要求。

商务服务网页表单已移除重复的“服务项目”和“企业名称”，企业信息在会员中心“用户资料”的商家公开资料区维护。API 为兼容既有程序仍保留原字段约定：商务服务 `attributes.service` 必填，`attributes.company` 可选，以 catalog 返回的 fields 为准；网页编辑不会清空这些历史值。

联系方式省略且用户资料未完善时返回 422。显式传入空值不会回退到用户资料。内容已取消价格和计价单位；旧程序传入的相关参数会被忽略，不影响已有信息的历史数据，也不影响订单和套餐金额。联系方式按提交时保存，修改用户资料不自动覆盖历史信息，相同 external_id 的重试也不更新原联系方式。

示例 payload（city_id/category_id 先替换为 catalog 返回值）：

商家编号在会员中心“用户资料”的商家公开资料区查看，仍为 business_id，不要与用户 UID 混用。API 不创建商家、不修改对外电话或公开授权；这些操作须本人在会员中心完成并通过审核。商家信息的公开电话来自商家资料，不会把本接口的 contact_phone 自动公开；个人联系人仍按原规则保存。省略 business_id 的旧程序不受影响。商家修改资料、隐藏电话或被下架后，网页同步停止展示其公开电话，原信息仍保留；相同 external_id 的重试仍返回原信息，不触发重新发布。接口响应新增 business_id，个人信息为 null。

```json
{
  "external_id": "source-article-10001",
  "city_id": 1,
  "category_id": 1,
  "subcategory": "cleaning",
  "title": "同城家庭保洁服务预约信息",
  "description": "此处填写真实服务范围、具体项目、预约方式以及双方需要确认的服务事项。",
  "summary": "服务范围、预约方式和注意事项介绍。",
  "contact_name": "联系人",
  "contact_phone": "13800000000",
  "attributes": {"service": "家庭保洁", "coverage": "填写真实服务区域"}
}
```

```bash
curl 'https://www.350c.com/api/v1/listings' \
  -H "Authorization: Bearer $LINJI_API_KEY" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  --data-binary @payload.json
```

首次成功返回 HTTP 201：

```json
{"data":{"id":123,"title":"同城家庭保洁服务预约信息","status":"pending","status_label":"待审核","rejection_reason":null,"business_id":null,"city_id":1,"district_code":null,"category_id":1,"subcategory":"cleaning","subcategory_type":null,"content_version":1,"summary":"服务范围、预约方式和注意事项介绍。","cover_mode":"none","cover_images":[],"column_id":null,"created_at":"2026-09-21T10:00:00+08:00","published_at":null},"replayed":false}
```

此示例不代表已发布成功。审核通过后才公开，账号发布的内容仍需真实、合法、具备相应授权。

## 上传正文图片与封面

先上传图片，再将返回的 `data.src` 写入正文图片节点或 `cover_images`。没有图片、没有封面也可正常发布。仅有封面而正文是纯文本时，保留 `description` 并传封面字段即可，不必构造富文本。

```bash
curl --silent --show-error --connect-timeout 10 --max-time 60 \
  'https://www.350c.com/api/v1/media' \
  -H "Authorization: Bearer $LINJI_API_KEY" \
  -H 'Accept: application/json' \
  -F 'image=@photo.jpg'
```

由 curl 自动设置 multipart 的 Content-Type 和 boundary，不要手写 `Content-Type: application/json`。每次仅一个 `image` 文件，不接受其他字段。支持 JPEG、PNG、WebP，单张不超过 4MB、宽高均不超过 4096；图片会重新编码为 JPEG，去除原始元数据。上传请求整体不超过 5MB，代理/PHP 限制也可能更严格。

成功返回 HTTP 201，例如：

```json
{"data":{"id":"123e4567-e89b-42d3-a456-426614174000","src":"/media/listings/123e4567-e89b-42d3-a456-426614174000.jpg","preview_url":"https://www.350c.com/api/v1/media/123e4567-e89b-42d3-a456-426614174000","expires_at":"2026-10-12T12:00:00+08:00"}}
```

这里的 UUID 仅为示例，请使用真实响应。上传记录 `id` 与图片文件名没有对应关系；发布引用 `src`，预览使用 `preview_url` 或 `/media/{id}`。`src` 是平台稳定引用，不是浏览器直接访问的 COS 地址。后台启用 COS 后会沿用同一配置上传到 COS；未启用时使用私有本地存储。不要保存临时 COS 签名地址作为正文图片。

上传后 24 小时内须在一次成功发布中引用；未引用的过期图片定期移入私有回收目录。每会员最多同时有 40 张未引用、未清理的图片，每分钟最多上传 10 次。上传不消耗发布额度，正常记录接口请求；失败后重新上传会生成新记录，请保留成功响应，避免反复上传。单篇正文与封面最多引用 20 张不同图片，同一图片同时用于正文和封面只计一张。

图片成功绑定文章后不再按上传时的 24 小时到期，也不允许用于第二篇新文章。文章校验失败不绑定图片、不消耗额度，可修改参数继续使用有效图片。文章审核前只允许本人持密钥预览，不能通过公开图片地址绕过审核；公开后按网站既有图片权限提供展示。图片从文章中移除、文章永久删除或进入回收站后，原 API 预览可能返回 404。

```bash
curl --silent --show-error \
  'https://www.350c.com/api/v1/media/真实上传记录ID' \
  -H "Authorization: Bearer $LINJI_API_KEY" \
  -H 'Accept: application/json' \
  --output preview.jpg
```

预览成功返回 HTTP 200 和 `Content-Type: image/jpeg`，不是 JSON；失败仍返回 JSON，请先检查状态码再将响应视为图片。预览接口不跳转到其他域名，不需要把密钥交给 COS。

## 富文本与封面示例

`body_document` 的根节点为 `doc`。支持段落 paragraph、二/三级标题 heading（level 2/3）、无序列表 bulletList、有序列表 orderedList（start 1–1000）、列表项 listItem、引用 blockquote、分隔线 horizontalRule、换行 hardBreak、图片 image 和文本 text。

文本 marks 支持 bold、italic、underline、strike、link；链接只接受合法 HTTP/HTTPS 地址，不允许 javascript、data 或带用户名密码的链接。未知节点、非法层级和非本人图片会被拒绝，多余样式/事件属性不会保存。最大嵌套 16 层、最多 2000 个节点、单个文本节点最多 5000 字符；仍受完整 JSON 64KB 和提取正文 20–5000 字符限制。仅有图片没有足够文字的文章不能发布。

以下为“正文插图 + 单图封面”完整示例。先把 city_id/category_id 替换为 catalog 的值，将两个 `src` 替换为上传返回值；同一张图片可同时用于正文和封面。

```json
{
  "external_id": "rich-article-10002",
  "city_id": 1,
  "category_id": 1,
  "subcategory": "cleaning",
  "title": "家庭保洁服务项目与预约说明",
  "body_document": {
    "type": "doc",
    "content": [
      {"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"服务项目"}]},
      {"type":"paragraph","content":[{"type":"text","text":"请填写真实服务范围、具体项目、预约方式及双方需要确认的服务事项。","marks":[{"type":"bold"}]}]},
      {"type":"image","attrs":{"src":"/media/listings/123e4567-e89b-42d3-a456-426614174000.jpg","alt":"服务现场"}}
    ]
  },
  "summary": "服务项目、预约方式与注意事项。",
  "cover_mode": "single",
  "cover_images": ["/media/listings/123e4567-e89b-42d3-a456-426614174000.jpg"],
  "contact_name": "联系人",
  "contact_phone": "13800000000",
  "attributes": {"service":"家庭保洁"}
}
```

保存为 payload.json，使用前面的 POST /listings 命令提交。无需封面时省略封面字段，或传 `cover_mode: "none"` 和 `cover_images: []`；三图封面则使用 `triple`，并提供三张不同的已上传图片。封面仅用于文章列表，不会额外拼到正文末尾；正文图片仍在指定位置展示。响应的 `summary` 是处理后的实际摘要，`cover_mode`、`cover_images` 是保存后的封面配置。

## 选择自己的专栏

```bash
curl 'https://www.350c.com/api/v1/columns?page=1' \
  -H "Authorization: Bearer $LINJI_API_KEY" \
  -H 'Accept: application/json'
```

返回 `data` 数组，每项包含 `id`、`title`、`content_version`、`url`，以及 `meta.current_page`、`last_page`、`per_page`（20）。只返回本人已审核且公开的专栏，空数组表示当前没有可选专栏。未审核、已下架和他人的专栏不能选。

发布 JSON 中添加 `column_id`，值为上述 `id`。首次提交时绑定专栏，但待审核文章不会在公开专栏里展示；文章审核通过并符合公开条件后才显示。一篇只允许一个专栏，发布后调整归属从会员中心完成。`column_id` 省略或 null 表示不指定专栏；不会自动创建专栏或自动通过审核。

## 重试与状态

`external_id` 在同一会员下唯一，跨密钥也去重。同编号、相同内容重试返回 HTTP 200 和 `replayed: true`，不新增、不重新审核。同编号不同内容返回 409，不覆盖原文；修改请从会员中心编辑。JSON 对象键顺序不影响去重，但字段增删、值类型或内容变化视为不同请求。校验失败不占用编号。网络超时/5xx 可以使用原编号及原内容重试，切勿每次重试生成新编号。

```bash
curl 'https://www.350c.com/api/v1/listings/123' \
  -H "Authorization: Bearer $LINJI_API_KEY" \
  -H 'Accept: application/json'
```

返回 `data.status`（pending/published/rejected/draft/withdrawn/trashed）、`status_label`（包括过期状态）和 `rejection_reason`，另包含 `summary`、`cover_mode`、`cover_images`（稳定 src 引用数组）和 `column_id`（未归属专栏时 null）。这些字段也随首次提交和重试响应返回。响应不返回完整正文、联系方式或图片文件。历史记录没有明确封面选择时，封面字段返回 none/空数组，不把旧图片自动当成新选封面。

`trashed` 表示移入回收站，不等同于永久删除；永久删除后查询返回 404、原编号重试返回 410。成功提交后的相同内容重试不重新校验图片的上传有效期或专栏的当前公开状态，也不会因为图片已经绑定而失败；返回的是该文章当前编辑状态，不是新发布。字段顺序可以调整，但图片数组和正文节点数组的顺序必须保持原样。

查询及重试返回会员当前编辑版本的标题、分类、状态、退回原因与 `content_version`。已发布文章再次编辑待审时，API 可返回 `pending`，但网页列表和详情仍保留上一个已审核版本，不会因为编辑待审变成 404。不要仅根据此时的 `pending` 判定旧版已下架。

时间为带时区 ISO 8601；`created_at` 是首次创建时间，`published_at` 是首次审核发布时间。从未发布的 `published_at` 为 null；已发布文章编辑待审或被下架后，该值仍可非 null，不代表当前编辑版已经通过审核。再次查询得到当前状态，不会把原来的待审核结果误认为最新状态。

## 错误和限流

错误统一为 JSON：`{"message":"说明","errors":{"title":["具体原因"]}}`。errors 仅校验失败时提供。

| HTTP | 含义 |
| --- | --- |
| 400 | 非法 JSON 或正文不是对象 |
| 401 | 缺少、错误、过期或已撤销密钥 |
| 403 | 账号/安全版本失效、权限不足或生产环境未使用 HTTPS |
| 404 | 信息/图片不存在、不属于当前会员或已不能预览 |
| 409 | 相同 external_id 已用于其他内容 |
| 410 | 重试编号对应的信息已被删除，或未引用的上传图片已过期 |
| 413 | JSON 请求超过 64KB，或 multipart 上传请求超过 5MB |
| 415 | 不支持的 Content-Type；发布用 JSON，上传图片用 multipart |
| 422 | 字段校验失败，修正 errors 指定字段 |
| 429 | 超过频率限制，按 Retry-After 响应头等待 |
| 405 | 路径存在，但使用了不支持的请求方法 |
| 500 | 服务端错误，稍后用同编号重试 |

每 IP 最多 120 请求/分钟，每会员最多 60 请求/分钟，发布频率以会员中心账号权限为准（默认 15 请求/分钟，重试也计入），图片上传另限每会员 10 次/分钟。额度仅成功首次发布时扣减，图片上传/预览和专栏查询不扣。重置密钥不会清空会员限额、上传图片归属或 external_id 去重记录。管理员可停用账号 API 或调整频率。服务器/反向代理可能有更严格的限制。本机未进行生产并发压测；生产使用需持久共享限流缓存、数据库并发验收和 HTTPS 配置。
