SpringBoot 单据自定义字段实现
方案:业务主表+JSON自定义字段,元数据统一配置,无冗余大表,高性能,适配多厂家动态字段
一、数据库SQL脚本
1. 自定义字段元数据表(全局配置小表)
sql
CREATE TABLE sys_biz_custom_field (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
biz_table VARCHAR(64) NOT NULL COMMENT '业务表名 如mes_production_order',
factory_code VARCHAR(64) DEFAULT '' COMMENT '厂家编码,空为全局通用',
field_key VARCHAR(64) NOT NULL COMMENT '自定义字段唯一标识',
field_name VARCHAR(100) NOT NULL COMMENT '字段展示名称',
field_type VARCHAR(32) NOT NULL COMMENT '字段类型:text/number/date/select/textarea',
dict_type VARCHAR(64) DEFAULT '' COMMENT '若依字典类型',
field_default VARCHAR(500) DEFAULT '' COMMENT '默认值',
is_required CHAR(1) DEFAULT '0' COMMENT '是否必填 0否1是',
sort_num INT DEFAULT 0 COMMENT '排序号',
status CHAR(1) DEFAULT '0' COMMENT '状态0正常1停用',
remark VARCHAR(500) DEFAULT '' COMMENT '备注',
create_by VARCHAR(64) DEFAULT '',
create_time DATETIME DEFAULT NULL,
update_by VARCHAR(64) DEFAULT '',
update_time DATETIME DEFAULT NULL,
del_flag CHAR(1) DEFAULT '0' COMMENT '删除标志',
PRIMARY KEY (id),
UNIQUE INDEX idx_table_factory_key (biz_table,factory_code,field_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务自定义字段元数据表';
2. MES生产工单主表(新增JSON自定义字段)
sql
CREATE TABLE mes_production_order (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
order_no VARCHAR(50) NOT NULL COMMENT '工单编号',
product_id BIGINT NOT NULL COMMENT '产品ID',
product_name VARCHAR(100) DEFAULT '' COMMENT '产品名称',
plan_num DECIMAL(10,2) DEFAULT 0 COMMENT '计划生产数量',
finish_num DECIMAL(10,2) DEFAULT 0 COMMENT '已完成数量',
factory_code VARCHAR(64) DEFAULT '' COMMENT '所属厂家编码',
order_status TINYINT DEFAULT 0 COMMENT '工单状态',
start_time DATETIME DEFAULT NULL COMMENT '计划开工时间',
end_time DATETIME DEFAULT NULL COMMENT '计划完工时间',
-- 核心JSON自定义字段
custom_field_json JSON NULL COMMENT '动态自定义字段集合',
create_by VARCHAR(64) DEFAULT '',
create_time DATETIME DEFAULT NULL,
update_by VARCHAR(64) DEFAULT '',
update_time DATETIME DEFAULT NULL,
del_flag CHAR(1) DEFAULT '0',
PRIMARY KEY (id),
INDEX idx_factory_code (factory_code),
INDEX idx_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES生产工单主表';
二、Maven依赖(若依已内置,确认即可)
xml
<!-- JSON类型处理器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- fastjson2 序列化 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
三、后端实体类
1. 自定义字段元数据实体 SysBizCustomField.java
java
package com.ruoyi.mes.domain;
import com.ruoyi.common.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("sys_biz_custom_field")
public class SysBizCustomField extends BaseEntity {
private Long id;
/** 业务表名 */
private String bizTable;
/** 厂家编码 */
private String factoryCode;
/** 字段标识 */
private String fieldKey;
/** 字段名称 */
private String fieldName;
/** 字段类型 */
private String fieldType;
/** 字典类型 */
private String dictType;
/** 默认值 */
private String fieldDefault;
/** 是否必填 */
private String isRequired;
/** 排序 */
private Integer sortNum;
/** 状态 */
private String status;
/** 备注 */
private String remark;
}
2. 生产工单实体 MesProductionOrder.java
java
package com.ruoyi.mes.domain;
import com.alibaba.fastjson2.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Map;
@Data
@TableName("mes_production_order")
public class MesProductionOrder extends BaseEntity {
private Long id;
/** 工单编号 */
private String orderNo;
/** 产品ID */
private Long productId;
/** 产品名称 */
private String productName;
/** 计划数量 */
private BigDecimal planNum;
/** 完成数量 */
private BigDecimal finishNum;
/** 厂家编码 */
private String factoryCode;
/** 工单状态 */
private Integer orderStatus;
/** 开工时间 */
private Date startTime;
/** 完工时间 */
private Date endTime;
/** JSON自定义字段 */
@TableField(typeHandler = com.ruoyi.common.handler.MapTypeHandler.class)
private Map<String, Object> customFieldJson;
/** 非数据库字段:前端接收自定义字段专用 */
@TableField(exist = false)
@JSONField(serialize = false)
private Map<String, Object> customFieldMap;
}
3. 全局JSON Map类型处理器 MapTypeHandler.java
java
package com.ruoyi.common.handler;
import com.alibaba.fastjson2.JSON;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
@MappedTypes(Map.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class MapTypeHandler extends BaseTypeHandler<Map<String,Object>> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Map<String, Object> parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, JSON.toJSONString(parameter));
}
@Override
public Map<String, Object> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return strToMap(rs.getString(columnName));
}
@Override
public Map<String, Object> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return strToMap(rs.getString(columnIndex));
}
@Override
public Map<String, Object> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return strToMap(cs.getString(columnIndex));
}
private Map<String,Object> strToMap(String str){
if(str == null || str.isEmpty()){
return new HashMap<>();
}
return JSON.parseObject(str, Map.class);
}
}
四、Mapper层
1. 自定义字段元数据 Mapper
java
package com.ruoyi.mes.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.mes.domain.SysBizCustomField;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface SysBizCustomFieldMapper extends BaseMapper<SysBizCustomField> {
/**
* 根据表名+厂家查询可用自定义字段
*/
List<SysBizCustomField> selectFieldList(@Param("bizTable") String bizTable, @Param("factoryCode") String factoryCode);
}
2. 生产工单 Mapper
java
package com.ruoyi.mes.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.mes.domain.MesProductionOrder;
public interface MesProductionOrderMapper extends BaseMapper<MesProductionOrder> {
}
五、Service层
1. 自定义字段Service
java
package com.ruoyi.mes.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.mes.domain.SysBizCustomField;
import java.util.List;
public interface ISysBizCustomFieldService extends IService<SysBizCustomField> {
List<SysBizCustomField> getFieldByTableAndFactory(String bizTable, String factoryCode);
}
java
package com.ruoyi.mes.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.mes.domain.SysBizCustomField;
import com.ruoyi.mes.mapper.SysBizCustomFieldMapper;
import com.ruoyi.mes.service.ISysBizCustomFieldService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SysBizCustomFieldServiceImpl extends ServiceImpl<SysBizCustomFieldMapper, SysBizCustomField>
implements ISysBizCustomFieldService {
@Override
public List<SysBizCustomField> getFieldByTableAndFactory(String bizTable, String factoryCode) {
return baseMapper.selectFieldList(bizTable,factoryCode);
}
}
2. 生产工单Service(核心:自动赋值JSON自定义字段)
java
package com.ruoyi.mes.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.mes.domain.MesProductionOrder;
public interface IMesProductionOrderService extends IService<MesProductionOrder> {
/**
* 保存工单+自动封装自定义字段到JSON
*/
boolean saveOrderWithCustomField(MesProductionOrder order);
/**
* 修改工单+更新自定义字段
*/
boolean updateOrderWithCustomField(MesProductionOrder order);
}
java
package com.ruoyi.mes.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.mes.domain.MesProductionOrder;
import com.ruoyi.mes.mapper.MesProductionOrderMapper;
import com.ruoyi.mes.service.IMesProductionOrderService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
@Service
public class MesProductionOrderServiceImpl extends ServiceImpl<MesProductionOrderMapper, MesProductionOrder>
implements IMesProductionOrderService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveOrderWithCustomField(MesProductionOrder order) {
// 将前端传入的自定义字段Map 赋值给JSON字段
Map<String,Object> customMap = order.getCustomFieldMap();
if(customMap == null){
customMap = new HashMap<>();
}
order.setCustomFieldJson(customMap);
// 清空临时字段,避免冗余
order.setCustomFieldMap(null);
return this.save(order);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateOrderWithCustomField(MesProductionOrder order) {
Map<String,Object> customMap = order.getCustomFieldMap();
if(customMap == null){
customMap = new HashMap<>();
}
order.setCustomFieldJson(customMap);
order.setCustomFieldMap(null);
return this.updateById(order);
}
}
六、Controller层
1. 自定义字段配置控制器
java
package com.ruoyi.mes.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.mes.domain.SysBizCustomField;
import com.ruoyi.mes.service.ISysBizCustomFieldService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/mes/custom/field")
public class SysBizCustomFieldController extends BaseController {
@Autowired
private ISysBizCustomFieldService sysBizCustomFieldService;
/**
* 获取单据对应厂家的所有自定义字段配置
*/
@GetMapping("/list")
public AjaxResult getCustomFieldList(@RequestParam String bizTable,@RequestParam String factoryCode){
List<SysBizCustomField> list = sysBizCustomFieldService.getFieldByTableAndFactory(bizTable,factoryCode);
return AjaxResult.success(list);
}
/**
* 新增自定义字段配置
*/
@PostMapping
public AjaxResult add(@RequestBody SysBizCustomField field){
return toAjax(sysBizCustomFieldService.save(field));
}
/**
* 修改自定义字段配置
*/
@PutMapping
public AjaxResult edit(@RequestBody SysBizCustomField field){
return toAjax(sysBizCustomFieldService.updateById(field));
}
/**
* 删除字段配置
*/
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids){
return toAjax(sysBizCustomFieldService.removeByIds(List.of(ids)));
}
}
2. 生产工单控制器
java
package com.ruoyi.mes.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.mes.domain.MesProductionOrder;
import com.ruoyi.mes.service.IMesProductionOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/mes/prod/order")
public class MesProductionOrderController extends BaseController {
@Autowired
private IMesProductionOrderService mesProductionOrderService;
/**
* 工单列表查询
*/
@GetMapping("/list")
public TableDataInfo list(MesProductionOrder order){
startPage();
List<MesProductionOrder> list = mesProductionOrderService.list();
return getDataTable(list);
}
/**
* 获取工单详情(自带JSON自定义字段)
*/
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable Long id){
return AjaxResult.success(mesProductionOrderService.getById(id));
}
/**
* 新增工单(携带自定义字段)
*/
@PostMapping
public AjaxResult add(@RequestBody MesProductionOrder order){
return toAjax(mesProductionOrderService.saveOrderWithCustomField(order));
}
/**
* 修改工单
*/
@PutMapping
public AjaxResult edit(@RequestBody MesProductionOrder order){
return toAjax(mesProductionOrderService.updateOrderWithCustomField(order));
}
/**
* 删除工单
*/
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids){
return toAjax(mesProductionOrderService.removeByIds(List.of(ids)));
}
}
七、XML查询语句(SysBizCustomFieldMapper.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.mes.mapper.SysBizCustomFieldMapper">
<select id="selectFieldList" resultType="com.ruoyi.mes.domain.SysBizCustomField">
SELECT * FROM sys_biz_custom_field
WHERE del_flag = '0' AND status = '0'
AND biz_table = #{bizTable}
AND (factory_code = #{factoryCode} OR factory_code = '')
ORDER BY sort_num ASC
</select>
</mapper>
八、核心特性说明
- 无大表压力:所有自定义字段内嵌主表JSON字段,不分通用大表
- 多厂家隔离 :通过
factory_code区分不同厂商单据字段配置 - 零改表结构:前端配置字段,无需重启服务、无需改实体
- 若依原生兼容:字典、分页、权限、导出全部原生支持
- 查询高效:单表查询,无联表,千万级数据无性能瓶颈
- 前端适配简单 :通过
/mes/custom/field/list接口拉取配置动态渲染表单
九、前端对接要点
- 新增/编辑页先调用字段配置接口,根据当前厂家+表名拿到所有自定义字段
- 动态渲染输入框、日期、下拉字典组件
- 表单自定义数据统一存入
customFieldMap - 提交后端自动转JSON存入
custom_field_json字段 - 回显直接读取实体中
customFieldJson赋值表单即可