# 任务大厅 & 任务详情 —— 对外开放接口文档 > **版本**:v1.0 > **最后更新**:2026-06-30 > **测试环境-基础URL**:`https://api.zjmjyy.com/api/api-platform` --- ## 目录 1. [认证方式(验签规则)](#认证方式验签规则) 2. [接口一:任务大厅列表](#接口一任务大厅列表) 3. [接口二:任务详情](#接口二任务详情) 4. [通用响应结构](#通用响应结构) 5. [错误码说明](#错误码说明) 6. [附录:Java 签名实现参考](#附录java-签名实现参考) --- ## 认证方式(验签规则) ### 概述 本文档仅开放 [接口一:任务大厅列表] [接口二:任务详情]对外接口**免登录** , 但必须通过 **签名验证**。每个请求需携带 3 个自定义 Header,服务端会校验合法性。 平台其它所有接口都需要**登录** ### 前置条件 联系平台管理员获取以下凭证(由平台分配并录入 `api_platform_info` 表): | 参数 | 说明 | 示例 | |------|------|------| | `appKey` | 平台身份标识 | `(填你的 appKey)` | | `appPublicKey` | 签名密钥(机密,切勿泄露) | `(填你的 appPublicKey)` | ### 请求 Header | Header 名称 | 必填 | 说明 | |-------------|------|------| | `X-App-Key` | 是 | 平台分配的身份标识 | | `X-Timestamp` | 是 | 当前时间戳(毫秒),有效期 ±5 分钟 | | `X-Sign` | 是 | 请求签名,生成算法见下方 | | `Authorization` | 否 | 小程序端使用,携带此 Header 时跳过验签 | ### 签名算法 ``` 签名原串 = 所有 Query 参数按 key 字典序拼接 "k1=v1&k2=v2&...×tamp={ts}&key={appPublicKey}" 签名结果 = MD5(签名原串) ``` **步骤详解:** 1. 取当前请求的全部 Query 参数(即 URL 中 `?` 后面的部分,不含路径参数) 2. 将参数按照 **key 的字母序升序排列**(Java 中用 `TreeMap`) 3. 拼接为 `key1=value1&key2=value2&...` 的形式(值为空则跳过该参数) 4. 在末尾追加 `timestamp={时间戳}&key={appPublicKey}` 5. 对整个字符串计算 **MD5**,取 **32 位小写十六进制**字符串 ### 签名示例 以**任务大厅列表**接口为例,假设请求参数为 `current=1&size=10`: ``` 步骤1: 原始参数 → current=1, size=10 步骤2: 字典序排列 → current=1, size=10 (本身就是升序) 步骤3: 拼接参数字符串 → "current=1&size=10&" 步骤4: 追加timestamp和key → "current=1&size=10×tamp=1751289600000&key=70c1b93ae1df463809ed3b41156e4791" 步骤5: MD5 → "3f8c5d2a1b9e4f6c8d0a2b4c6e8f0a1b" ``` 最终请求: ```http GET /task/visitor/hall?current=1&size=10 HTTP/1.1 Host: 121.199.7.40:9998 X-App-Key: 填你的 appKey X-Timestamp: 1751289600000 X-Sign: 3f8c5d2a1b9e4f6c8d0a2b4c6e8f0a1b ``` ### 签名注意事项 1. **参数排序**:必须按 key 的 `String` 自然排序(字典序/ASCII 序),Java 用 `TreeMap` 即可保证 2. **空参数跳过**:值为 `null` 或空字符串 `""` 的参数**不参与签名** 3. **路径参数不参与签名**:`@PathVariable`(如 `/{taskId}` 中的 taskId)不在 Query String 中,**不参与签名** 4. **时间戳有效期**:服务端允许的时间偏差为 **±5 分钟**,超时请求返回 `请求已过期` 5. **签名格式**:MD5 输出 **32 位小写十六进制**字符串 6. **编码**:签名原串使用 **UTF-8** 编码后计算 MD5 7. **小程序兼容**:若请求携带 `Authorization` Header(JWT Token),则直接放行,不触发验签 --- ## 接口一:任务大厅列表 ### 基本信息 | 项目 | 内容 | |------|----------------------| | **接口路径** | `/task/visitor/hall` | | **请求方式** | `GET` | | **认证** | 免登录 + 签名验证 | | **Content-Type** | 无需(GET 请求) | ### 请求参数(Query String) | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | `current` | int | 否 | 页码,默认 1 | | `size` | int | 否 | 每页条数,默认 10 | | `occupationCode` | String | 否 | 职业编码 | | `lng` | Double | 否 | 经度 | | `lat` | Double | 否 | 纬度 | | `provinceId` | String | 否 | 省 ID | | `cityId` | String | 否 | 市 ID | | `areaId` | String | 否 | 区 ID | | `streetId` | String | 否 | 街道 ID | | `titleFuzzy` | String | 否 | 任务标题模糊搜索 | | `timeRange` | String | 否 | 时间范围:`LastWeek`(最近一周)、`LastHalfYear`(最近半年)、不传或 `ALL`(不限) | | `sortByType` | int | 否 | 排序类型:`1`=创建时间倒序,`2`=距离升序 | | `sortField` | String | 否 | 自定义排序字段(如 `wage`=工资待遇) | | `sortOrder` | String | 否 | 排序规则:`asc`=升序,`desc`=降序 | ### 响应参数 返回分页结构,`records` 中每个元素字段如下: | 字段 | 类型 | 说明 | |------|------|------| | `taskId` | String | 任务 ID | | `bizId` | String | 业务身份 ID | | `bizType` | Integer | 业务域:0=个人,1=一般企业 | | `bizName` | String | 业务身份名称 | | `workTimeLimit` | Integer | 是否限制作业时间:0=不限制,1=限制 | | `title` | String | 任务标题 | | `description` | String | 任务描述 | | `taskAddress` | String | 任务工作地址 | | `provinceId` / `provinceName` | String | 省 ID / 名称 | | `cityId` / `cityName` | String | 市 ID / 名称 | | `areaId` / `areaName` | String | 区 ID / 名称 | | `streetId` / `streetName` | String | 街道 ID / 名称 | | `lat` / `lng` | Double | 纬度 / 经度 | | `contactPhone` | String | 联系人手机号 | | `email` | String | 联系人邮箱 | | `settlementUnit` | Integer | 结算单位:1=元/小时,2=元/天,3=元/次,4=元/件 | | `occupationCode` | String | 职业编码 | | `occupationName` | String | 职业名称 | | `wage` | BigDecimal | 佣金 | | `requiredPersonnel` | Integer | 需要人数 | | `welfare` | String | 任务福利 | | `taskType` | Integer | 任务类型:1=撮合,2=代发 | | `taskStatus` | Integer | 任务状态:1=代付款,2=待开始,3=已超时,4=进行中,5=已完成,6=已取消 | | `taskCycleStartTime` | Date | 任务周期起始时间(`yyyy-MM-dd HH:mm:ss`) | | `taskCycleEndTime` | Date | 任务周期结束时间(`yyyy-MM-dd HH:mm:ss`) | | `certificateRequired` | Integer | 是否需要证书:0=不需要,1=需要 | | `certificate` | String | 证书列表,多个逗号分隔 | | `insureMethod` | Integer | 保险方式:1=发单方购买,2=双方线下承保 | | `insureEffectType` | Integer | 保险购买方式:0=暂不购买,2=按打卡购买,3=按任务周期购买 | | `insuranceType` | String | 险种类别 | | `distance` | String | 距离(米) | | `isPersonal` | Integer | 发单方身份:0=个人,1=企业 | | `applyCount` | Integer | 报名人数 | | `taskNumber` | String | 任务编号 | | `onDutyTime` / `offDutyTime` | String | 到场时间 / 离场时间 | | `gmtCreated` | Date | 创建时间(`yyyy-MM-dd HH:mm:ss`) | | `createTime` | String | 创建时间字符串(兼容字段) | ### 请求示例 ```bash curl -X GET \ 'https://api.zjmjyy.com/api/api-platform/task/visitor/hall?current=1&size=10' \ -H 'X-App-Key: 填你的 appKey' \ -H 'X-Timestamp: 1751289600000' \ -H 'X-Sign: 3f8c5d2a1b9e4f6c8d0a2b4c6e8f0a1b' ``` ### 响应示例 ```json { "code": 200, "msg": "操作成功", "data": { "current": 1, "size": 10, "total": 156, "pages": 16, "records": [ { "taskId": "TASK20260601001", "bizId": "BIZ001", "bizType": 1, "bizName": "XX建筑公司", "title": "招电工-日结", "description": "需要持证电工,工作地点XX工地", "taskAddress": "浙江省杭州市余杭区XX路XX号", "provinceId": "330000", "provinceName": "浙江省", "cityId": "330100", "cityName": "杭州市", "areaId": "330110", "areaName": "余杭区", "lat": 30.2500, "lng": 120.1300, "occupationCode": "OCC001", "occupationName": "电工", "wage": 350.00, "requiredPersonnel": 5, "taskType": 1, "taskStatus": 4, "taskCycleStartTime": "2026-06-15 08:00:00", "taskCycleEndTime": "2026-07-15 18:00:00", "settlementUnit": 1, "distance": "1250.50", "isPersonal": 1, "applyCount": 12, "gmtCreated": "2026-06-01 10:30:00" } ] } } ``` --- ## 接口二:任务详情 ### 基本信息 | 项目 | 内容 | |------|------| | **接口路径** | `/task/visitor/{taskId}` | | **请求方式** | `GET` | | **认证** | 免登录 + 签名验证 | | **Content-Type** | 无需(GET 请求) | ### 请求参数 | 参数名 | 类型 | 位置 | 必填 | 说明 | |--------|------|------|------|------| | `taskId` | String | Path | 是 | 任务 ID(从任务大厅列表获取) | > ⚠️ **重要**:`taskId` 是路径参数(`@PathVariable`),**不参与签名计算**。 > 当该接口没有 Query 参数时,签名原串为:`timestamp={ts}&key={appPublicKey}` ### 响应参数 | 字段 | 类型 | 说明 | |------|------|------| | `taskId` | String | 任务 ID | | `bizId` | String | 业务身份 ID | | `bizType` | Integer | 业务域:0=个人,1=企业 | | `bizName` | String | 业务身份名称 | | `title` | String | 任务标题 | | `description` | String | 任务描述 | | `workTimeLimit` | Integer | 是否限制作业时间:0=不限制,1=限制 | | `onDutyTime` / `offDutyTime` | String | 到场时间 / 离场时间 | | `taskAddress` | String | 任务工作地址 | | `provinceId` / `provinceName` | String | 省 ID / 名称 | | `cityId` / `cityName` | String | 市 ID / 名称 | | `areaId` / `areaName` | String | 区 ID / 名称 | | `streetId` / `streetName` | String | 街道 ID / 名称 | | `lat` / `lng` | Double | 纬度 / 经度 | | `contactPhone` | String | 联系人手机号 | | `contact` | String | 联系人 | | `welfare` | String | 任务福利 | | `occupationCode` | String | 职业编码 | | `occupationName` | String | 职业名称 | | `settlementUnit` | Integer | 结算单位:1=元/小时,2=元/天,3=元/次,4=元/件 | | `taskCycle` | Integer | 任务周期:1=长期,2=短期 | | `taskCycleStartTime` | Date | 任务周期起始时间(`yyyy-MM-dd`) | | `taskCycleEndTime` | Date | 任务周期结束时间(`yyyy-MM-dd`) | | `wage` | BigDecimal | 佣金 | | `requiredPersonnel` | Integer | 需要人数 | | `taskType` | Integer | 任务类型:1=撮合,2=代发 | | `taskStatus` | Integer | 任务状态:1=代付款,2=待开始,3=已超时,4=进行中,5=已完成,6=已取消 | | `taskCompletionTime` | Date | 任务完成时间 | | `taskActualStartTime` | Date | 任务实际开始时间 | | `taskCancellationTime` | Date | 任务取消时间 | | `taskTimeout` | Date | 任务超时时间 | | `certificateRequired` | Integer | 是否需要证书:0=不需要,1=需要 | | `certificate` | String | 证书列表 | | `sexLimit` | Integer | 性别限制:1=不限,2=男,3=女 | | `ageLimit` | Integer | 是否限制年龄:0=不限制,1=限制 | | `minAge` / `maxAge` | Integer | 最小/最大年龄限制 | | `insureMethod` | Integer | 保险方式:1=发单方购买,2=双方线下承保 | | `insureEffectType` | Integer | 保险购买方式:0=暂不购买,2=按打卡购买,3=按任务周期购买 | | `insuranceType` | String | 险种类别 | | `isFaceCheckInRequired` | Integer | 是否需要人脸打卡:0=不需要,1=需要 | | `checkInRangeLimit` | Integer | 是否限制打卡范围:0=不限,1=限制 | | `checkRange` | Long | 打卡范围(米) | | `distance` | String | 距离(米) | | `applyCount` | Integer | 报名人数 | | `isPersonal` | Integer | 发单方身份:0=个人,1=企业 | | `taskNumber` | String | 任务编号 | | `days` | Integer | 任务天数 | | `taskCycleEndTimeOver` | Boolean | 是否已过任务周期结束时间 | | `hasDevice` | Boolean | 是否包含打卡机 | | `attendanceGroupId` | String | 打卡组 ID | | `companyIntro` | String | 公司简介 | | `jobRequirements` | String | 招聘条件 | ### 请求示例 ```bash curl -X GET \ 'https://api.zjmjyy.com/api/api-platform/task/visitor/TASK20260601001' \ -H 'X-App-Key: 填你的 appKey' \ -H 'X-Timestamp: 1751289600000' \ -H 'X-Sign: 8a2d4e6f1b3c5d7e9f0a2b4c6d8e0f1a' ``` ### 响应示例 ```json { "code": 200, "msg": "操作成功", "data": { "taskId": "TASK20260601001", "bizId": "BIZ001", "bizType": 1, "bizName": "XX建筑公司", "title": "招电工-日结", "description": "需要持证电工,工作地点XX工地,每天8小时", "workTimeLimit": 1, "onDutyTime": "08:00", "offDutyTime": "18:00", "taskAddress": "浙江省杭州市余杭区XX路XX号", "provinceId": "330000", "provinceName": "浙江省", "cityId": "330100", "cityName": "杭州市", "areaId": "330110", "areaName": "余杭区", "lat": 30.2500, "lng": 120.1300, "contactPhone": "138****1234", "contact": "张经理", "occupationCode": "OCC001", "occupationName": "电工", "settlementUnit": 1, "taskCycle": 2, "taskCycleStartTime": "2026-06-15", "taskCycleEndTime": "2026-07-15", "wage": 350.00, "requiredPersonnel": 5, "taskType": 1, "taskStatus": 4, "sexLimit": 1, "ageLimit": 1, "minAge": 25, "maxAge": 55, "certificateRequired": 1, "certificate": "电工证", "insureMethod": 1, "insureEffectType": 2, "isPersonal": 1, "applyCount": 12, "distance": "1250.50", "companyIntro": "XX建筑公司成立于2000年...", "jobRequirements": "持有有效电工证,3年以上工作经验" } } ``` --- ## 通用响应结构 所有接口统一返回: ```json { "code": 200, "msg": "操作成功", "data": { ... } } ``` | 字段 | 类型 | 说明 | |------|------|------| | `code` | int | 状态码,`200` 表示成功 | | `msg` | String | 提示信息 | | `data` | Object | 业务数据 | **分页响应 `data` 额外包含:** | 字段 | 类型 | 说明 | |------|------|------| | `current` | int | 当前页码 | | `size` | int | 每页条数 | | `total` | int | 总记录数 | | `pages` | int | 总页数 | | `records` | Array | 数据列表 | --- ## 错误码说明 ### 签名相关错误 | 错误信息 | 原因 | 解决方案 | |----------|------|----------| | `缺少验签参数` | 请求未携带 `X-App-Key`、`X-Timestamp` 或 `X-Sign` | 确保三个 Header 全部传入且值非空 | | `请求已过期` | `X-Timestamp` 与服务器时间差超过 ±5 分钟 | 生成新时间戳,重新计算签名后重试 | | `时间戳格式错误` | `X-Timestamp` 不是合法的长整型毫秒值 | 传入正确的毫秒时间戳,如 `1751289600000` | | `无效的AppKey` | `X-App-Key` 在平台不存在或已停用 | 检查 appKey 是否正确,联系管理员确认 | | `签名验证失败` | `X-Sign` 与服务端计算的不一致 | 检查签名算法实现,确认参数排序、拼接方式、MD5 结果 | ### 排查技巧 1. **打印签名原串**:在调用方代码中把签名原串打印出来,与服务端日志对比 2. **确认参数排序**:务必用 TreeMap / 字典序排序 3. **确认 MD5 格式**:32 位小写十六进制 4. **确认时间戳单位**:使用**毫秒**,不是秒 5. **确认编码**:签名字符串使用 UTF-8 编码后计算 MD5 --- ## 附录:Java 签名实现参考 以下是完整的 Java 签名工具代码,第三方可直接复制使用: ```java import java.security.MessageDigest; import java.util.*; public class ApiSignUtil { /** * 构建请求签名 * * @param params 请求参数 Map(query 参数,key=参数名,value=参数值) * @param timestamp 时间戳(毫秒) * @param secret 平台分配的 appPublicKey * @return 32位小写MD5签名字符串 */ public static String buildSign(Map params, long timestamp, String secret) { // 1. 参数字典序排列(TreeMap 自动排序) TreeMap sorted = new TreeMap<>(params); // 2. 拼接签名原串:k1=v1&k2=v2...×tamp=xxx&key=secret StringBuilder sb = new StringBuilder(); for (Map.Entry entry : sorted.entrySet()) { if (entry.getValue() != null && !entry.getValue().isEmpty()) { sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } } sb.append("timestamp=").append(timestamp); sb.append("&key=").append(secret); // 3. MD5 → 32位小写十六进制 return md5(sb.toString()); } private static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.reset(); md.update(input.getBytes("UTF-8")); byte[] digest = md.digest(); StringBuilder sb = new StringBuilder(digest.length * 2); for (byte b : digest) { sb.append(String.format("%02x", b & 0xff)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException("MD5 error", e); } } // ────────── 使用示例 ────────── public static void main(String[] args) { String appKey = "AK_eaf449b6fb9f21"; String secret = "70c1b93ae1df463809ed3b41156e4791"; // 示例1:任务大厅列表 Map hallParams = new LinkedHashMap<>(); hallParams.put("current", "1"); hallParams.put("size", "10"); hallParams.put("occupationCode", "OCC001"); long ts = System.currentTimeMillis(); String sign = buildSign(hallParams, ts, secret); System.out.println("X-App-Key : " + appKey); System.out.println("X-Timestamp : " + ts); System.out.println("X-Sign : " + sign); // 示例2:任务详情(无Query参数) Map emptyParams = new LinkedHashMap<>(); long ts2 = System.currentTimeMillis(); String sign2 = buildSign(emptyParams, ts2, secret); System.out.println("\n[任务详情] X-Timestamp: " + ts2); System.out.println("[任务详情] X-Sign : " + sign2); } } ```