灵活用工项目

API开放接口文档-V1.md 18KB

任务大厅 & 任务详情 —— 对外开放接口文档

版本:v1.0
最后更新:2026-06-30 测试环境-基础URLhttps://api.zjmjyy.com/api/api-platform


目录

  1. 认证方式(验签规则)
  2. 接口一:任务大厅列表
  3. 接口二:任务详情
  4. 通用响应结构
  5. 错误码说明
  6. 附录: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&...&timestamp={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&timestamp=1751289600000&key=70c1b93ae1df463809ed3b41156e4791"
步骤5: MD5           → "3f8c5d2a1b9e4f6c8d0a2b4c6e8f0a1b"

最终请求:

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 创建时间字符串(兼容字段)

请求示例

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'

响应示例

{
  "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 招聘条件

请求示例

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'

响应示例

{
  "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年以上工作经验"
  }
}

通用响应结构

所有接口统一返回:

{
  "code": 200,
  "msg": "操作成功",
  "data": { ... }
}
字段 类型 说明
code int 状态码,200 表示成功
msg String 提示信息
data Object 业务数据

分页响应 data 额外包含:

字段 类型 说明
current int 当前页码
size int 每页条数
total int 总记录数
pages int 总页数
records Array 数据列表

错误码说明

签名相关错误

错误信息 原因 解决方案
缺少验签参数 请求未携带 X-App-KeyX-TimestampX-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 签名工具代码,第三方可直接复制使用:

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<String, String> params, long timestamp, String secret) {
        // 1. 参数字典序排列(TreeMap 自动排序)
        TreeMap<String, String> sorted = new TreeMap<>(params);

        // 2. 拼接签名原串:k1=v1&k2=v2...&timestamp=xxx&key=secret
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> 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<String, String> 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<String, String> 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);
    }
}