lead
6 天以前 bc00242ee0410684eb9f4c5fe0aa486dfebb3162
fix(api): 修复全量接口扫描发现的 500 —— 由 70 个降到 0 个

基于 /v3/api-docs 自动发现 4173 个接口做全量探测(scripts/api-500-probe/probe_500.py),
第一轮 70 个返回 500,逐一定位修复后复测为 0。

1. 导出类接口(Excel 写不出去)
- 新增 LocalTimeConvert:FastExcel 不支持 LocalTime,班次/考勤规则导出直接 500
- 质检方案 types(List<Integer>)、生产任务 pauseRecords/assigneeUserIds/assigneeUserNames
改用 JsonConvert 承载,避免 Can not find 'Converter' support class List

2. 归档台账导出(MybatisPlusException: can't found IPage for args!)
- exportArchiveExcel 传 Page 为 null,改为按 pageNo/pageSize 构造 Page

3. BPM 流程定义
- page:sort 为 null 时 Comparator.comparing 抛 NPE,改用 nullsLast
- get:id 与 key 都为空时给出业务提示,不再让 Flowable 抛 key is null

4. 文件下载(CacheLoader.InvalidCacheLoadException)
- configId 无效时按 404 返回,不再是 500

5. 全局异常处理
- MultipartException(导入接口未传文件)→ 400
- 其它 HttpMessageNotReadableException(报文结构不匹配)→ 400
- buildExceptionLog 对 NOT NULL 列做空值兜底,避免异常日志本身插入失败

6. 请求体缺少必填校验(空请求体直接打到 DB 报 Field 'xxx' doesn't have a default value)
- MDM 物料/计量单位/品牌/分类/仓库 SaveReqVO 加 @NotBlank
- MES 资料审核 SaveReqVO 加 @NotBlank/@NotNull
- BI 图表配置/数据源配置 DO 加 @NotBlank/@NotNull
- 示例 demo03 course/grade DO 加 @NotNull

已完成第二轮验证:4173 个接口中 500 数量为 0;导出类接口实际返回 xlsx 字节流。
已添加11个文件
已修改19个文件
484 ■■■■■ 文件已修改
scripts/api-500-probe/map_500.py 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/api-500-probe/map_dive.py 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/api-500-probe/map_rest.py 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/api-500-probe/map_types.py 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/api-500-probe/probe_500.py 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/api-500-probe/probe_500_detail.py 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/api-500-probe/replay.py 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/api-500-probe/replay_list.txt 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/api-500-probe/replay_list2.txt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/api-500-probe/replay_list3.txt 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalTimeConvert.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/dataobject/BiChartConfigDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/dataobject/BiDataSourceConfigDO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/brand/vo/MdmBrandSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategorySaveReqVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/warehouse/vo/MdmWarehouseSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/archive/MesPdArchiveController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/documentaudit/vo/MesPdDocumentAuditSaveReqVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/vo/MesQcTemplateRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/api-500-probe/map_500.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""从容器日志里把每个 500 å¼‚常映射到接口 + é¦–个业务栈帧"""
import re, sys, collections
path = sys.argv[1]
cur_url = ""
records = []
buf = None
with open(path, encoding="utf-8", errors="replace") as f:
    for line in f:
        line = re.sub(r"\x1b\[[0-9;]*m", "", line.rstrip("\n"))
        m = re.search(r"\[preHandle\]\[开始请求 URL\(([^)]+)\)", line)
        if m:
            cur_url = m.group(1)
            continue
        if "defaultExceptionHandler" in line:
            buf = {"url": cur_url, "exc": "", "frames": []}
            records.append(buf)
            continue
        if buf is not None:
            if not buf["exc"] and re.match(r"^[a-zA-Z].*(Exception|Error)", line):
                buf["exc"] = line.strip()[:200]
            elif line.startswith("\tat cn.iocoder") and len(buf["frames"]) < 3:
                buf["frames"].append(line.strip()[:180])
            if line.startswith("\tat ") and len(buf["frames"]) >= 3:
                buf = None
agg = collections.OrderedDict()
for r in records:
    key = r["url"].split("?")[0]
    agg.setdefault(key, {"exc": r["exc"], "frame": (r["frames"][0] if r["frames"] else ""), "n": 0})
    agg[key]["n"] += 1
for url, v in agg.items():
    print("%s\n    EXC: %s\n    FRAME: %s  (x%d)" % (url, v["exc"], v["frame"], v["n"]))
print("URLS", len(agg))
scripts/api-500-probe/map_dive.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""对 DataIntegrityViolationException æŠ“取具体 SQL ä¸Žçº¦æŸå"""
import re, subprocess, sys
text = subprocess.run(["docker", "logs", "--since", "15m", "mom-kyzn"],
                      stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=180).stdout.decode("utf-8", "replace")
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
lines = text.split("\n")
i = 0
cur = ""
out = []
while i < len(lines):
    m = re.search(r"\[preHandle\]\[开始请求 URL\(([^)]+)\)", lines[i])
    if m:
        cur = m.group(1).split("?")[0]
    if "DataIntegrityViolationException" in lines[i] or "SQLIntegrityConstraintViolationException" in lines[i] or "doesn't have a default value" in lines[i] or "cannot be null" in lines[i]:
        ctx = []
        for l in lines[max(0, i-3): i+6]:
            if "INSERT INTO" in l or "doesn't have a default value" in l or "cannot be null" in l or "Data too long" in l or "Duplicate entry" in l or "Field " in l:
                ctx.append(l.strip()[:300])
        if ctx:
            out.append((cur, ctx))
    i += 1
seen = set()
for cur, ctx in out:
    key = cur
    if key in seen:
        continue
    seen.add(key)
    print("###", cur)
    for c in ctx[:3]:
        print("   ", c)
print("TOTAL", len(out), "UNIQUE", len(seen))
scripts/api-500-probe/map_rest.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
import re, subprocess
t = subprocess.run(["docker","logs","--since","30m","mom-kyzn"],stdout=subprocess.PIPE,stderr=subprocess.STDOUT,timeout=180).stdout.decode("utf-8","replace")
t = re.sub(r"\x1b\[[0-9;]*m","",t)
ls = t.split("\n")
cur=""
for i,l in enumerate(ls):
    m = re.search(r"\[preHandle\]\[开始请求 URL\(([^)]+)\)", l)
    if m: cur=m.group(1).split("?")[0]
    if "defaultExceptionHandler" in l:
        for l2 in ls[i+1:i+60]:
            if re.match(r"^[a-zA-Z].*?(Exception|Error)", l2):
                print(cur, "|", l2.strip()[:150]); break
scripts/api-500-probe/map_types.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
import re, subprocess
text = subprocess.run(["docker","logs","--since","30m","mom-kyzn"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=180).stdout.decode("utf-8","replace")
text = re.sub(r"\x1b\[[0-9;]*m","",text)
lines = text.split("\n")
want = ["HttpMessageNotWritableException","IORuntimeException","InvalidCacheLoadException","MybatisPlusException","NullPointerException","FlowableIllegalArgumentException","HttpMessageNotReadableException"]
cur=""
res={}
for i,l in enumerate(lines):
    m = re.search(r"\[preHandle\]\[开始请求 URL\(([^)]+)\)", l)
    if m: cur = m.group(1).split("?")[0]
    for w in want:
        if w in l:
            res.setdefault(w, set()).add(cur)
for w in want:
    if w in res:
        print("##", w)
        for u in sorted(res[w]):
            print("   ", u)
scripts/api-500-probe/probe_500.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""康意 mom-kyzn å…¨é‡æŽ¥å£ 500 æ‰«æï¼ˆåŸºäºŽ /v3/api-docs è‡ªåŠ¨å‘çŽ°ï¼‰"""
import json, sys, re, time, urllib.request, urllib.error, concurrent.futures
BASE = "http://127.0.0.1:9007"
TOKEN = sys.argv[1]
TENANT = "1"
def load_doc(url):
    req = urllib.request.Request(url, headers={"Accept": "application/json"})
    with urllib.request.urlopen(req, timeout=120) as r:
        return json.loads(r.read().decode("utf-8"))
def collect():
    cfg = load_doc(BASE + "/v3/api-docs/swagger-config")
    ops = []
    seen = set()
    for u in cfg.get("urls", []):
        try:
            doc = load_doc(BASE + u["url"])
        except Exception as e:
            print("SKIP doc", u["url"], e)
            continue
        paths = doc.get("paths", {})
        for path, methods in paths.items():
            for m, op in methods.items():
                if m.lower() not in ("get", "post", "put", "delete"):
                    continue
                if path in seen:
                    continue
                seen.add(path + " " + m)
                ops.append((m.upper(), path, op.get("operationId", "")))
    return ops
def call(op):
    m, path, oid = op
    # æ›¿æ¢è·¯å¾„参数为 1
    real = re.sub(r"\{[^/}]+\}", "1", path)
    url = BASE + real
    data = None
    headers = {"Authorization": "Bearer " + TOKEN, "tenant-id": TENANT}
    if m in ("POST", "PUT"):
        data = b"{}"
        headers["Content-Type"] = "application/json"
    req = urllib.request.Request(url, data=data, headers=headers, method=m)
    t0 = time.time()
    try:
        with urllib.request.urlopen(req, timeout=60) as r:
            body = r.read(4000).decode("utf-8", "replace")
            code = r.status
    except urllib.error.HTTPError as e:
        body = e.read(4000).decode("utf-8", "replace")
        code = e.code
    except Exception as e:
        return (m, path, oid, "EXC", str(e)[:120], int((time.time()-t0)*1000))
    try:
        j = json.loads(body)
        biz = j.get("code")
        msg = (j.get("msg") or "")[:160]
    except Exception:
        biz, msg = None, body[:160]
    return (m, path, oid, biz if biz is not None else code, msg, int((time.time()-t0)*1000))
def main():
    ops = collect()
    print("TOTAL_OPS", len(ops), flush=True)
    results = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
        for r in ex.map(call, ops):
            results.append(r)
    bad500 = [r for r in results if r[3] == 500 or r[3] == "EXC"]
    others = {}
    for r in results:
        others[str(r[3])] = others.get(str(r[3]), 0) + 1
    print("STATS", json.dumps(others, ensure_ascii=False), flush=True)
    print("===== 500 LIST =====", flush=True)
    for r in sorted(bad500, key=lambda x: x[1]):
        print("%s %s | op=%s | code=%s | %s | %sms" % r, flush=True)
    print("500_COUNT", len(bad500), flush=True)
    with open("/tmp/kyzn_probe_all.json", "w") as f:
        json.dump([{"method": r[0], "path": r[1], "op": r[2], "code": r[3], "msg": r[4], "ms": r[5]} for r in results], f, ensure_ascii=False, indent=1)
main()
scripts/api-500-probe/probe_500_detail.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""对 500 æŽ¥å£é€ä¸ªé‡æ”¾ï¼Œå¹¶ç²¾ç¡®æŠ“取对应的异常类型与业务栈帧"""
import json, re, subprocess, sys, time, urllib.request, urllib.error
from datetime import datetime
BASE = "http://127.0.0.1:9007"
TOKEN = sys.argv[1]
data = json.load(open("/tmp/kyzn_probe_all.json"))
targets = []
seen = set()
for r in data:
    if r["code"] != 500:
        continue
    key = (r["method"], r["path"])
    if key in seen:
        continue
    seen.add(key)
    targets.append(key)
def call(m, path):
    real = re.sub(r"\{[^/}]+\}", "1", path)
    headers = {"Authorization": "Bearer " + TOKEN, "tenant-id": "1"}
    body = None
    if m in ("POST", "PUT"):
        body = b"{}"
        headers["Content-Type"] = "application/json"
    req = urllib.request.Request(BASE + real, data=body, headers=headers, method=m)
    try:
        with urllib.request.urlopen(req, timeout=60) as r:
            return json.loads(r.read(3000).decode("utf-8", "replace")).get("code")
    except urllib.error.HTTPError as e:
        try:
            return json.loads(e.read(3000).decode("utf-8", "replace")).get("code")
        except Exception:
            return e.code
    except Exception as e:
        return "EXC:" + str(e)[:60]
def exception_since(ts):
    out = subprocess.run(["docker", "logs", "--since", ts, "mom-kyzn"],
                         stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120)
    text = re.sub(r"\x1b\[[0-9;]*m", "", out.stdout.decode("utf-8", "replace"))
    lines = text.split("\n")
    exc = ""
    frames = []
    for i, l in enumerate(lines):
        if "defaultExceptionHandler" in l:
            for l2 in lines[i+1:i+400]:
                if not exc and re.match(r"^[a-zA-Z].*?(Exception|Error)", l2):
                    exc = l2.strip()[:180]
                elif l2.startswith("\tat cn.iocoder") and len(frames) < 2:
                    frames.append(l2.strip()[:150])
                if len(frames) >= 2 and exc:
                    break
            if exc:
                break
    return exc, frames
results = []
for m, path in targets:
    ts = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
    time.sleep(0.2)
    code = call(m, path)
    time.sleep(0.6)
    exc, frames = exception_since(ts)
    results.append({"method": m, "path": path, "code": code, "exc": exc, "frames": frames})
    print("%-58s %-6s %s" % (path, code, exc), flush=True)
json.dump(results, open("/tmp/kyzn_500_detail.json", "w"), ensure_ascii=False, indent=1)
scripts/api-500-probe/replay.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
import json, re, subprocess, sys, time, urllib.request, urllib.error
from datetime import datetime
BASE="http://127.0.0.1:9007"; TOKEN=sys.argv[1]
targets = [l.split() for l in sys.stdin.read().strip().split("\n") if l.strip()]
def call(m,p):
    real = re.sub(r"\{[^/}]+\}","1",p)
    h={"Authorization":"Bearer "+TOKEN,"tenant-id":"1"}; b=None
    if m in ("POST","PUT"):
        b=b"{}"; h["Content-Type"]="application/json"
    req=urllib.request.Request(BASE+real,data=b,headers=h,method=m)
    try:
        with urllib.request.urlopen(req,timeout=60) as r:
            raw=r.read(3000).decode("utf-8","replace")
    except urllib.error.HTTPError as e:
        raw=e.read(3000).decode("utf-8","replace")
    except Exception as e:
        return "EXC "+str(e)[:80]
    try:
        j=json.loads(raw); return "code=%s msg=%s" % (j.get("code"), (j.get("msg") or "")[:120])
    except Exception:
        return "RAW "+raw[:120]
def exc_since(ts):
    out=subprocess.run(["docker","logs","--since",ts,"mom-kyzn"],stdout=subprocess.PIPE,stderr=subprocess.STDOUT,timeout=120)
    t=re.sub(r"\x1b\[[0-9;]*m","",out.stdout.decode("utf-8","replace"))
    ls=t.split("\n")
    for i,l in enumerate(ls):
        if "defaultExceptionHandler" in l:
            for l2 in ls[i+1:i+120]:
                if re.match(r"^[a-zA-Z].*?(Exception|Error)", l2):
                    return l2.strip()[:200]
    return ""
for m,p in targets:
    ts=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"); time.sleep(0.2)
    r=call(m,p); time.sleep(0.6)
    print("%-52s %-58s %s" % (p, r, exc_since(ts)), flush=True)
scripts/api-500-probe/replay_list.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
GET /admin-api/system/dept/simple-list
GET /admin-api/system/dict-type/page
GET /admin-api/system/mail-account/page
GET /admin-api/srm/tender-bid/get
GET /admin-api/mes/dv/metering/simple-list
GET /admin-api/mes/md/unit-measure/export-excel
GET /admin-api/mes/pd/archive/export-excel
GET /admin-api/infra/file/1/get/a.png
GET /admin-api/bpm/process-definition/page
GET /admin-api/bpm/form/create
scripts/api-500-probe/replay_list2.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
GET /admin-api/mes/qc/template/export-excel
GET /admin-api/mes/pro/task/export-excel
GET /admin-api/mes/pd/archive/export-excel
GET /admin-api/hrm/shift/export-excel
GET /admin-api/hrm/attendance/rule/export-excel
POST /admin-api/mdm/item/create
POST /admin-api/bi/chart-config/create
GET /admin-api/infra/file/1/get/a.png
GET /admin-api/bpm/process-definition/page
scripts/api-500-probe/replay_list3.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
GET /admin-api/mes/dv/repair/get
GET /admin-api/mes/dv/spare/get
GET /admin-api/srm/supplier-certificate/page
GET /admin-api/system/dept/simple-list
GET /admin-api/system/mail-account/page
GET /admin-api/crm/customer-limit-config/get
GET /admin-api/im/manager/face-pack-item/page
GET /admin-api/member/user/update-level
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalTimeConvert.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package cn.iocoder.yudao.framework.excel.core.convert;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
 * Excel LocalTime è½¬æ¢å™¨
 *
 * FastExcel å†…置只支持 Date / LocalDateTime,遇到 LocalTime å­—段会抛
 * {@code ExcelWriteDataConvertException: Can not find 'Converter' support class LocalTime},
 * å› æ­¤è¿™é‡Œè¡¥ä¸€ä¸ªè½¬æ¢å™¨ï¼Œä¾›ç­æ¬¡ã€è€ƒå‹¤è§„则等含时间字段的 VO å¯¼å‡ºæ—¶ä½¿ç”¨ã€‚
 *
 * @author è¶…级管理员
 */
public class LocalTimeConvert implements Converter<LocalTime> {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
    @Override
    public Class<?> supportJavaTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        throw new UnsupportedOperationException("暂不支持,也不需要");
    }
    @Override
    public WriteCellData<String> convertToExcelData(LocalTime value, ExcelContentProperty contentProperty,
                                                    GlobalConfiguration globalConfiguration) {
        if (value == null) {
            return new WriteCellData<>("");
        }
        return new WriteCellData<>(value.format(FORMATTER));
    }
}
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
@@ -36,6 +36,7 @@
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import tools.jackson.databind.exc.InvalidFormatException;
@@ -96,6 +97,10 @@
        }
        if (ex instanceof MaxUploadSizeExceededException) {
            return maxUploadSizeExceededExceptionHandler((MaxUploadSizeExceededException) ex);
        }
        // ä¸Šä¼ ç±»æŽ¥å£æ”¶åˆ°éž multipart è¯·æ±‚(前端漏传文件 / ç”¨ JSON è°ƒç”¨äº†å¯¼å…¥æŽ¥å£ï¼‰å±žäºŽå®¢æˆ·ç«¯é—®é¢˜
        if (ex instanceof MultipartException) {
            return multipartExceptionHandler((MultipartException) ex);
        }
        if (ex instanceof NoHandlerFoundException) {
            return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
@@ -170,6 +175,17 @@
    }
    /**
     * å¤„理文件上传接口收到非 multipart è¯·æ±‚
     *
     * ä¾‹å¦‚说,Excel å¯¼å…¥æŽ¥å£æ¼ä¼  file å­—段、或前端用 application/json è°ƒç”¨äº†å¯¼å…¥æŽ¥å£
     */
    @ExceptionHandler(MultipartException.class)
    public CommonResult<?> multipartExceptionHandler(MultipartException ex) {
        log.warn("[multipartExceptionHandler]", ex);
        return CommonResult.error(BAD_REQUEST.getCode(), "请求参数不正确:请使用 multipart/form-data ä¸Šä¼ æ–‡ä»¶");
    }
    /**
     * å¤„理 SpringMVC å‚数绑定不正确,本质上也是通过 Validator æ ¡éªŒ
     */
    @ExceptionHandler(BindException.class)
@@ -196,7 +212,9 @@
        if (StrUtil.startWith(ex.getMessage(), "Required request body is missing")) {
            return CommonResult.error(BAD_REQUEST.getCode(), "请求参数类型错误: request body ç¼ºå¤±");
        }
        return defaultExceptionHandler(ServletUtils.getRequest(), ex);
        // å…¶å®ƒååºåˆ—化失败(如把对象传成数组、字段类型不匹配)同样是客户端报文问题,不应按 500 å¤„理
        return CommonResult.error(BAD_REQUEST.getCode(),
                String.format("请求参数类型错误:%s", StrUtil.maxLength(ex.getMessage(), 120)));
    }
    /**
@@ -374,27 +392,31 @@
        errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
        // è®¾ç½®å¼‚常字段
        errorLog.setExceptionName(e.getClass().getName());
        errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
        errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
        errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e));
        // infra_api_error_log çš„这些列都是 NOT NULL;而 MyBatis-Plus æ’入时会跳过 null å­—段,
        // äºŽæ˜¯ä»»ä¸€å­—段为 null éƒ½ä¼šå˜æˆ "Field 'xxx' doesn't have a default value",异常日志反而记不下来
        errorLog.setExceptionMessage(StrUtil.nullToEmpty(ExceptionUtil.getMessage(e)));
        errorLog.setExceptionRootCauseMessage(StrUtil.nullToEmpty(ExceptionUtil.getRootCauseMessage(e)));
        errorLog.setExceptionStackTrace(StrUtil.nullToEmpty(ExceptionUtil.stacktraceToString(e)));
        StackTraceElement[] stackTraceElements = e.getStackTrace();
        Assert.notEmpty(stackTraceElements, "异常 stackTraceElements ä¸èƒ½ä¸ºç©º");
        StackTraceElement stackTraceElement = stackTraceElements[0];
        errorLog.setExceptionClassName(stackTraceElement.getClassName());
        errorLog.setExceptionFileName(stackTraceElement.getFileName());
        errorLog.setExceptionMethodName(stackTraceElement.getMethodName());
        // æ ˆé¡¶å…ƒç´ å¯èƒ½æ˜¯ native æ–¹æ³•或 Lambda,getFileName() ä¸º null;
        // infra_api_error_log çš„这些列是 NOT NULL,为 null ä¼šå¯¼è‡´å¼‚常日志插入失败(日志里只剩一条 SQL æŠ¥é”™ï¼‰
        errorLog.setExceptionFileName(StrUtil.nullToEmpty(stackTraceElement.getFileName()));
        errorLog.setExceptionMethodName(StrUtil.nullToEmpty(stackTraceElement.getMethodName()));
        errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
        // è®¾ç½®å…¶å®ƒå­—段
        errorLog.setTraceId(TracerUtils.getTraceId());
        errorLog.setApplicationName(applicationName);
        errorLog.setRequestUrl(request.getRequestURI());
        errorLog.setTraceId(StrUtil.nullToEmpty(TracerUtils.getTraceId()));
        errorLog.setApplicationName(StrUtil.nullToEmpty(applicationName));
        errorLog.setRequestUrl(StrUtil.nullToEmpty(request.getRequestURI()));
        Map<String, Object> requestParams = MapUtil.<String, Object>builder()
                .put("query", ServletUtils.getParamMap(request))
                .put("body", ServletUtils.getBody(request)).build();
        errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
        errorLog.setRequestMethod(request.getMethod());
        errorLog.setUserAgent(ServletUtils.getUserAgent(request));
        errorLog.setUserIp(ServletUtils.getClientIP(request));
        errorLog.setRequestParams(StrUtil.nullToEmpty(JsonUtils.toJsonString(requestParams)));
        errorLog.setRequestMethod(StrUtil.nullToEmpty(request.getMethod()));
        errorLog.setUserAgent(StrUtil.nullToEmpty(ServletUtils.getUserAgent(request)));
        errorLog.setUserIp(StrUtil.nullToEmpty(ServletUtils.getClientIP(request)));
        errorLog.setExceptionTime(LocalDateTime.now());
    }
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/dataobject/BiChartConfigDO.java
@@ -4,6 +4,8 @@
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.*;
/**
@@ -29,16 +31,19 @@
     *
     * å…³è” {@link BiDashboardDO#getId()}
     */
    @NotNull(message = "所属仪表盘ID不能为空")
    private Long dashboardId;
    /**
     * å›¾è¡¨åç§°
     */
    @NotBlank(message = "图表名称不能为空")
    private String name;
    /**
     * å›¾è¡¨ç±»åž‹ï¼šbar / line / pie / scatter / gauge / table / number
     */
    @NotBlank(message = "图表类型不能为空")
    private String chartType;
    /**
yudao-module-bi/src/main/java/cn/iocoder/yudao/module/bi/dal/dataobject/BiDataSourceConfigDO.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.NotBlank;
import lombok.*;
/**
@@ -27,6 +28,7 @@
    /**
     * æ•°æ®æºåç§°
     */
    @NotBlank(message = "数据源名称不能为空")
    private String name;
    /**
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
@@ -119,7 +120,11 @@
    public CommonResult<BpmProcessDefinitionRespVO> getProcessDefinition(
            @RequestParam(value = "id", required = false) String id,
            @RequestParam(value = "key", required = false) String key) {
        ProcessDefinition processDefinition = id != null ? processDefinitionService.getProcessDefinition(id)
        // id ä¸Ž key è‡³å°‘要传一个,否则 Flowable ä¼šæŠ› FlowableIllegalArgumentException(key is null) å˜æˆ 500
        if (StrUtil.isBlank(id) && StrUtil.isBlank(key)) {
            throw new IllegalArgumentException("流程编号 id ä¸Žæµç¨‹å®šä¹‰æ ‡è¯† key ä¸èƒ½åŒæ—¶ä¸ºç©º");
        }
        ProcessDefinition processDefinition = StrUtil.isNotBlank(id) ? processDefinitionService.getProcessDefinition(id)
                : processDefinitionService.getActiveProcessDefinition(key);
        if (processDefinition == null) {
            return success(null);
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java
@@ -57,8 +57,10 @@
            BpmCategoryDO category = MapUtil.get(categoryMap, definition.getCategory(), BpmCategoryDO.class);
            return buildProcessDefinition(definition, deployment, processDefinitionInfo, form, category, null);
        });
        // æŽ’序
        result.sort(Comparator.comparing(BpmProcessDefinitionRespVO::getSort));
        // æŽ’序:sort æ¥è‡ªæµç¨‹å®šä¹‰æ‰©å±•信息,历史数据可能为 null,
        // ç›´æŽ¥ç”¨ Comparator.comparing ä¼šæŠ› NPE(接口 500),这里把 null æŽ’到最后
        result.sort(Comparator.comparing(BpmProcessDefinitionRespVO::getSort,
                Comparator.<Long>nullsLast(Comparator.<Long>naturalOrder())));
        return result;
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleRespVO.java
@@ -4,6 +4,7 @@
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.framework.excel.core.convert.LocalTimeConvert;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -32,11 +33,11 @@
    private String deptName;
    @Schema(description = "上班时间")
    @ExcelProperty("上班时间")
    @ExcelProperty(value = "上班时间", converter = LocalTimeConvert.class)
    private LocalTime workStartTime;
    @Schema(description = "下班时间")
    @ExcelProperty("下班时间")
    @ExcelProperty(value = "下班时间", converter = LocalTimeConvert.class)
    private LocalTime workEndTime;
    @Schema(description = "迟到允许分钟数")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmShiftRespVO.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import cn.iocoder.yudao.framework.excel.core.convert.LocalTimeConvert;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -23,11 +24,11 @@
    private String name;
    @Schema(description = "上班时间")
    @ExcelProperty("上班时间")
    @ExcelProperty(value = "上班时间", converter = LocalTimeConvert.class)
    private LocalTime startTime;
    @Schema(description = "下班时间")
    @ExcelProperty("下班时间")
    @ExcelProperty(value = "下班时间", converter = LocalTimeConvert.class)
    private LocalTime endTime;
    @Schema(description = "标准工时(小时)")
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
@@ -11,6 +11,7 @@
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import cn.iocoder.yudao.module.infra.service.file.FileService;
import com.google.common.cache.CacheLoader;
import com.google.common.util.concurrent.UncheckedExecutionException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -126,6 +127,11 @@
            log.warn("[getFileContent][configId({}) path({}) æ–‡ä»¶é…ç½®ä¸å­˜åœ¨]", configId, path);
            response.setStatus(HttpStatus.NOT_FOUND.value());
            return;
        } catch (CacheLoader.InvalidCacheLoadException ex) {
            // LoadingCache çš„ load è¿”回 null æ—¶æŠ›å‡ºï¼ˆä¾‹å¦‚历史文件记录的 configId å·²è¢«åˆ é™¤ï¼‰ï¼ŒåŒæ ·æŒ‰èµ„源不存在处理
            log.warn("[getFileContent][configId({}) path({}) æ–‡ä»¶é…ç½®ä¸å­˜åœ¨]", configId, path);
            response.setStatus(HttpStatus.NOT_FOUND.value());
            return;
        }
        if (content == null) {
            log.warn("[getFileContent][configId({}) path({}) æ–‡ä»¶ä¸å­˜åœ¨]", configId, path);
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.NotNull;
import lombok.*;
/**
@@ -29,6 +30,7 @@
    /**
     * å­¦ç”Ÿç¼–号
     */
    @NotNull(message = "学生编号不能为空")
    private Long studentId;
    /**
     * åå­—
@@ -37,6 +39,7 @@
    /**
     * åˆ†æ•°
     */
    @NotNull(message = "分数不能为空")
    private Integer score;
}
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.NotNull;
import lombok.*;
/**
@@ -29,6 +30,7 @@
    /**
     * å­¦ç”Ÿç¼–号
     */
    @NotNull(message = "学生编号不能为空")
    private Long studentId;
    /**
     * åå­—
@@ -37,6 +39,7 @@
    /**
     * ç­ä¸»ä»»
     */
    @NotNull(message = "班主任不能为空")
    private String teacher;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/brand/vo/MdmBrandSaveReqVO.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mdm.controller.admin.brand.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Schema(description = "管理后台 - MDM å“ç‰Œæ–°å¢ž/修改 Request VO")
@@ -11,9 +12,11 @@
    private Long id;
    @Schema(description = "品牌编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "BRAND001")
    @NotBlank(message = "品牌编码不能为空")
    private String code;
    @Schema(description = "品牌名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "品牌A")
    @NotBlank(message = "品牌名称不能为空")
    private String name;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategorySaveReqVO.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mdm.controller.admin.category.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Schema(description = "管理后台 - MDM ç‰©æ–™åˆ†ç±»æ–°å¢ž/修改 Request VO")
@@ -17,6 +18,7 @@
    private String code;
    @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "五金配件")
    @NotBlank(message = "分类名称不能为空")
    private String name;
    @Schema(description = "物料类型", example = "1")
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mdm.controller.admin.item.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.math.BigDecimal;
@@ -13,9 +14,11 @@
    private Long id;
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    @NotBlank(message = "物料编码不能为空")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    @NotBlank(message = "物料名称不能为空")
    private String name;
    @Schema(description = "物料条码", example = "12345678")
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mdm.controller.admin.unit.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.math.BigDecimal;
@@ -13,9 +14,11 @@
    private Long id;
    @Schema(description = "单位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "PCS")
    @NotBlank(message = "单位编码不能为空")
    private String code;
    @Schema(description = "单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "个")
    @NotBlank(message = "单位名称不能为空")
    private String name;
    @Schema(description = "是否主单位", example = "true")
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/warehouse/vo/MdmWarehouseSaveReqVO.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mdm.controller.admin.warehouse.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.math.BigDecimal;
@@ -13,9 +14,11 @@
    private Long id;
    @Schema(description = "仓库编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "WH001")
    @NotBlank(message = "仓库编码不能为空")
    private String code;
    @Schema(description = "仓库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "成品仓库")
    @NotBlank(message = "仓库名称不能为空")
    private String name;
    @Schema(description = "仓库地址", example = "上海市浦东新区")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/archive/MesPdArchiveController.java
@@ -106,8 +106,11 @@
    @ApiAccessLog(operateType = EXPORT)
    public void exportArchiveExcel(@Valid MesPdArchivePageReqVO pageReqVO,
                                   HttpServletResponse response) throws IOException {
        // å¯¼å‡ºéœ€è¦ä¼ å…¥ Page å¯¹è±¡ï¼šåˆ†ç»„查询走 XML mapper,Page ä¸º null æ—¶ MyBatis-Plus ä¼šæŠ›
        // MybatisPlusException: can't found IPage for args!(此前这里传 null ç›´æŽ¥å¯¼è‡´ 500)
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        IPage<MesPdArchiveGroupRespVO> archivePage = archiveService.getArchivePageGroupByProject(pageReqVO, null);
        Page<MesPdArchiveGroupRespVO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
        IPage<MesPdArchiveGroupRespVO> archivePage = archiveService.getArchivePageGroupByProject(pageReqVO, page);
        ExcelUtils.write(response, "归档台账.xls", "数据", MesPdArchiveRespVO.class,
                buildArchiveRespVOList(archivePage.getRecords()));
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/documentaudit/vo/MesPdDocumentAuditSaveReqVO.java
@@ -15,9 +15,11 @@
    private Long id;
    @Schema(description = "审核单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "AUD2024001")
    @NotBlank(message = "审核单号不能为空")
    private String auditNo;
    @Schema(description = "资料ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "资料ID不能为空")
    private Long documentId;
    @Schema(description = "资料名称", example = "产品设计图")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java
@@ -1,5 +1,8 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo;
import cn.iocoder.yudao.framework.excel.core.convert.JsonConvert;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -9,6 +12,7 @@
@Schema(description = "管理后台 - MES ç”Ÿäº§ä»»åŠ¡ Response VO")
@Data
@ExcelIgnoreUnannotated
public class MesProTaskRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@@ -135,12 +139,16 @@
    private LocalDateTime createTime;
    @Schema(description = "停工历史记录")
    // å¯¼å‡º Excel æ—¶ä¸æ”¯æŒ List å­—段,这里改用 JsonConvert è½¬æˆ JSON æ–‡æœ¬ï¼Œé¿å…å¯¼å‡ºæŽ¥å£ 500
    @ExcelProperty(value = "停工历史记录", converter = JsonConvert.class)
    private List<MesProTaskPauseRecordRespVO> pauseRecords;
    @Schema(description = "指定报工人员 ID é›†åˆ")
    @ExcelProperty(value = "指定报工人员ID", converter = JsonConvert.class)
    private List<Long> assigneeUserIds;
    @Schema(description = "指定报工人员姓名集合")
    @ExcelProperty(value = "指定报工人员姓名", converter = JsonConvert.class)
    private List<String> assigneeUserNames;
    @Schema(description = "是否报工审批")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/vo/MesQcTemplateRespVO.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo;
import cn.iocoder.yudao.framework.excel.core.convert.JsonConvert;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -26,7 +27,7 @@
    private String name;
    @Schema(description = "检测种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 3]")
    @ExcelProperty("检测种类")
    @ExcelProperty(value = "检测种类", converter = JsonConvert.class)
    private List<Integer> types;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@@ -42,3 +43,4 @@
    private LocalDateTime createTime;
}