想要在其他地方上传图片到极态云,如何适配极态云的图片字段?

给桌面软件做一个插件,把某个图片上传到极态云的图片字段。

1、我发现极态云图片字段是有特殊的存储结构,
2、我发现在网页端操作的时候,oss的存储会在jit_storage表中有一条记录,然后在图片字段中存的好像是这个表的一条json值
3、如果我想要实现本地图片上传极态云的图片字段中,是否一定要在这个表中创建一条记录?是否一定要构造成某个格式,前端才能正常展示?

可以做,但不建议桌面软件直接操作 OSS、jit_storage 或业务模型表。推荐做法是:在极态云应用里封装一个后端服务函数,桌面软件只调用这个服务函数上传图片;服务函数负责上传文件、生成图片字段需要的 JSON,再更新模型字段。

结论

图片字段最终保存的不是图片二进制,也不是单独一个 URL,而是一个图片对象数组,例如:

[
  {
    "uid": "唯一值",
    "name": "存储后的文件名或对象名",
    "fileName": "原始文件名.png",
    "url": "https://...",
    "md5": "...",
    "size": 12345,
    "type": "image/png"
  }
]

前端正常展示主要依赖这个数组结构,尤其是 urlfileNameuidsizetype 这些字段。只存一个 URL,或者字段值不是数组,后续展示、预览、编辑都容易出问题。

jit_storage 对应的是平台文件记录。图片字段本身不要求你直接 SQL 插入 jit_storage,但如果希望和网页端上传后的文件管理、使用记录保持一致,应该通过平台接口或模型 API 创建这条文件记录,而不是手工写库。

推荐接口

可以在应用里建一个服务函数:

services/DesktopUploadSvc/uploadImageToModelField

桌面端请求地址通常是:

POST https://你的域名/api/<组织ID>/<应用ID>/services/DesktopUploadSvc/uploadImageToModelField

例如应用的 apiPath/api/whwy/demoapp,那完整地址就是:

https://你的域名/api/whwy/demoapp/services/DesktopUploadSvc/uploadImageToModelField

请求方式用 multipart/form-data

curl -X POST \
  -H "Authorization: Bearer <your-token>" \
  -F "file=@/path/to/a.png;type=image/png" \
  -F "modelFullName=xxx.models.Customer" \
  -F "pk=123" \
  -F "fieldName=avatar" \
  "https://你的域名/api/<组织ID>/<应用ID>/services/DesktopUploadSvc/uploadImageToModelField"

参数含义:

file: 本地图片文件
modelFullName: 要更新的模型 fullName,例如 xxx.models.Customer
pk: 要更新的记录主键
fieldName: 图片字段名

服务函数示例

示例逻辑是“上传一张图片并覆盖写入图片字段”。如果你的图片字段允许多张图片,需要先读取原字段数组,再把新图片对象追加进去,最后仍然整体更新这个字段。

# -*-coding:utf-8-*-
import hashlib
import mimetypes
import uuid
from datetime import datetime
from pathlib import Path

from services.NormalType import NormalService


class DesktopUploadSvc(NormalService):
    def uploadImageToModelField(self, file, modelFullName, pk, fieldName):
        fileItem = file[0] if isinstance(file, list) else file
        originalFileName = fileItem.filename
        data = fileItem.stream.read()

        if not data:
            return {"status": False, "error": "文件内容为空"}

        md5 = hashlib.md5(data).hexdigest()
        suffix = Path(originalFileName).suffix
        storedFileName = f"{md5}{suffix}"
        contentType = mimetypes.guess_type(originalFileName)[0] or "application/octet-stream"

        storageSvc = app.getElement("storages.services.StorageSvc")
        uploadResult = storageSvc.uploadByFile(
            originalFileName,
            data=data,
            md5=md5,
            contentType=contentType,
        )

        url = uploadResult.get("url") if isinstance(uploadResult, dict) else ""
        if not url:
            return {"status": False, "error": "文件上传失败,未返回 url"}

        storageObj = storageSvc.getStorage()
        storeFullName = getattr(storageObj, "name", "") or "storages.Default"

        imageUid = uuid.uuid4().hex
        imageItem = {
            "uid": imageUid,
            "name": storedFileName,
            "fileName": originalFileName,
            "url": url,
            "md5": md5,
            "size": len(data),
            "type": contentType,
            "storeFullName": storeFullName,
        }

        pkValue = int(pk) if isinstance(pk, str) and pk.isdigit() else pk

        # 可选但推荐:创建平台文件记录,保持和网页端上传链路一致。
        FileModel = app.getElement("storages.services.models.FileModel")
        fileRow = FileModel.create({
            "uid": imageUid,
            "fileName": originalFileName,
            "url": url,
            "storeFullName": storeFullName,
            "uploadTime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "md5": md5,
            "size": len(data),
        })
        if isinstance(fileRow, dict):
            imageItem.update(fileRow)

        model = app.getElement(modelFullName)
        model.updateByPK(
            pkList=[pkValue],
            updateData={fieldName: [imageItem]},
            triggerEvent=1,
        )

        return {
            "status": True,
            "data": imageItem,
        }

e.json 配置示例

服务函数需要在 services/DesktopUploadSvc/e.json 里声明。参数名要和 Python 函数一致:

{
  "title": "桌面端图片上传服务",
  "type": "services.NormalType",
  "backendBundleEntry": ".",
  "functionList": [
    {
      "name": "uploadImageToModelField",
      "title": "上传图片到模型图片字段",
      "args": [
        {"name": "file", "title": "图片文件", "dataType": "File"},
        {"name": "modelFullName", "title": "模型FullName", "dataType": "Stext"},
        {"name": "pk", "title": "主键", "dataType": "Stext"},
        {"name": "fieldName", "title": "图片字段名", "dataType": "Stext"}
      ],
      "returnType": "JitDict"
    }
  ]
}

多图追加

上面的示例是覆盖写入:

updateData={fieldName: [imageItem]}

如果要追加到多图字段,不要只传一个对象,也不要把原值覆盖掉。应该先查出当前记录的图片字段值,得到原数组后再追加:

pkValue = int(pk) if isinstance(pk, str) and pk.isdigit() else pk
rowObj = model.queryset.get(**{model.pkName: pkValue}, level=0)
oldItems = rowObj.value.get(fieldName) if rowObj else []
oldItems = oldItems or []

model.updateByPK(
    pkList=[pkValue],
    updateData={fieldName: oldItems + [imageItem]},
    triggerEvent=1,
)

外部调用鉴权

这个接口给桌面软件调用时,一定要处理鉴权。通常有两种方式:

  1. 使用平台的外部 API 授权方式调用。
  2. 如果你要自己用请求头 token 鉴权,可以关闭平台默认登录和签名校验,再用拦截器校验。

第二种方式需要在这个服务函数的 e.json 函数声明里加:

{
  "ignoreSign": true,
  "loginRequired": false
}

然后在请求拦截器里校验类似这样的请求头:

Authorization: Bearer <your-token>

注意:不要只关闭平台校验而不加自己的拦截器,否则上传接口会变成未鉴权接口。

另外,生产环境不建议完全相信客户端传入的 modelFullNamefieldName。最好在服务函数里做白名单,只允许更新指定模型的指定图片字段。

自定义请求鉴权可以参考:

简单总结

用户不需要直接写 OSS 和数据库表。正确路径是:

桌面软件上传图片
  -> 调用应用后端服务函数
  -> 服务函数调用 StorageSvc.uploadByFile 上传文件
  -> 服务函数按平台格式生成图片对象数组
  -> 可选但推荐:通过 FileModel.create 创建平台文件记录
  -> 服务函数调用 model.updateByPK 写入图片字段

这样前端图片字段可以正常展示,后续预览、编辑和文件管理链路也更接近网页端上传的行为。