可为空的值类型(Nullable<T>)需要注意的地方

肯赖罩籽多点DMALL数据平台的四次跃迁

多点 DMALL 的数据平台经过四次跃迁,始终围绕“更快、更省、更稳”展开。

在多点 DMALL 的数据平台建设过程中,先是借 AWS-EMR 快速构建云端大数据能力,再回归IDC 自建 Hadoop 集群,以开源内核叠加自研集成、调度、开发组件,把重资产沉淀为可复用的轻服务。当业务需要更低成本、更高弹性,团队用存算分离、容器化重构底座,引入 Apache SeaTunnel 让数据实时入湖;继而以 Apache Iceberg、Paimon 统一存储格式,形成湖仓一体的新架构,为 AI 提供稳态、低成本的数据基座,完成由借云到造云、由离线到实时的闭环。

存算分离架构

DMALL UniData(Data IDE)的存算分离架构以Kubernetes 为弹性基座,Spark、Flink、StarRocks 按需伸缩,Iceberg+JuiceFS 统一湖存储,Hive Metastore 跨云管理元数据,Ranger 细粒度授权,存算分离、零厂商绑定,技术栈全链路可控。

由此带来的业务收益水到渠成:TCO直降40-75%,资源秒级扩缩,同一套IDE框架覆盖集成、调度、建模、查询与服务,交付快、人力省,多云畅行且安全。

一、旧架构的痛点

在引入 Apache SeaTunnel 之前,多点 DMALL 数据平台数据互导已支持 MySQL、Hive、ES 等十余种存储自助式数据同步,基于 Spark 自研多种数据源,可按需求定制化接入,但仅支持批处理(Batch)模式。

在数据导入方面,多点 DMALL 数据平台统一承载公司 ODS 数据入湖,采用 Apache Iceberg 作为湖仓格式,支持小时级数据下游可用 ,数据复用率高,数据质量有保障。

过去我们依赖 Spark 自研同步工具,虽然稳定,却面临“启动慢、资源重、扩展难”的痛点。

“不是 Spark 不好,而是它太重了。”

在降本增效的大背景下,我们重新审视了原有的数据集成架构。Spark 批任务虽然成熟,但在处理中小规模数据同步时显得“杀鸡用牛刀”。启动慢、资源占用高、开发周期长,成为团队效率的瓶颈。更重要的是,面对越来越多实时性业务需求,Spark 的批处理模式已难以为继。

维度 旧 Spark 方案 业务影响

资源高 2C8G 起步,空跑 40s 对于大多数中小规模数据同步并不友好

开发高 缺 Source/Sink 抽象,全链路开发 全链路开发,增加开发与维护成本,降低交付效率

不支持实时同步 新技术兴起,实时增量同步需求增加 现阶段仍需要开发人员用 Java/Flink 自行实现

数据源有限 商家私有化部署增多,数据源多样 新增数据源定制开发,难以快速满足业务需求

直到我们遇见了 Apache SeaTunnel,一切开始改变。

二、为什么圈定SeaTunnel?

“我们不是在选工具,而是在选未来五年的数据集成底座。”

面对多样化的数据源、实时性需求和资源优化压力,我们需要一个“批流一体、轻量高效、易扩展”的集成平台。SeaTunnel 以其开源、多引擎支持、丰富的连接器和活跃社区,成为我们最终的选择。它不仅解决了 Spark 的“重”问题,还为未来的湖仓一体和实时分析打下了基础。

引擎中立:内置 Zeta,同时兼容 Spark/Flink,可随数据量自动切换。

连接器官网已 200+,且插件化:新增数据源只写 JSON,零 Java 代码。

批流一体:同一套配置即可全量+增量+CDC。

社区活跃:GitHub 8.8k star,PR 周合并 30+,我们提的 5 个 Patch 均在 7 天内合入主干。

三、新平台架构:让SeaTunnel长成“企业级”

“开源不是拿来就用,而是站在巨人肩膀上继续造轮子。”

SeaTunnel 虽然强大,但要真正落地企业级场景,还需要一层“外壳”——统一的管理、调度、权限、限流、监控等能力。我们围绕 SeaTunnel 构建了一套可视化、可配置、可扩展的数据集成平台,让它从一个开源工具,成长为多点数据平台的“核心引擎”。

3.1 全局架构

以 Apache SeaTunnel 为底座,平台向上透出统一 REST API,Web UI、商家交换、MCP 服务等任何外部系统都可一键调用;内置连接器模板中心,新存储只需填参数即可分钟级发布,无需编码。调度层同时适配 Apache DolphinScheduler、Airflow 等主流编排,引擎层按数据量智能路由 Zeta/Flink/Spark,小任务轻量快跑,大任务分布式并行;镜像与运行环境全面云原生化,支持 K8s、Yarn、Standalone 多模式输出,商家私有化场景也能一键交付,真正做到“模板即服务、引擎可切换、部署无绑定”。

3.2 互导功能

数据源注册:地址、账号、密码一次录入,敏感字段加密,公共数据源(如 Hive)全租户可见。

连接器模板:通过配置新增连接器,定义 SeaTunnel 配置生成规则,控制任务界面 Source、Sink 显示

离线任务:运行批任务支持 Zeta 和 Spark 引擎,通过 DAG 图描述同步任务,支持通配符变量注入

实时任务:运行流任务支持 Zeta 和 Flink 引擎,通过 S3 协议存储 Checkpoint,可进行 CDC 增量同步

接入功能

接入申请:用户提交同步表申请工单;管理员审批,以保障数据接入质量

库表管理:按库同步,避免同步链路过多;统一管理链路,数据质量有保障;支持分表合并成一张表

基线拉取:通过批任务进行自动建表和初始化;超大表可按照规则拆分拉取;数据缺失可基于条件拉取补齐

数据同步:同步任务通过REST API提交到集群;支持限流、打标特性保证重要同步;CDC增量写入多种湖仓

四、二次开发:让SeaTunnel说“多点方言”

“再优秀的开源项目,也听不懂你业务的‘方言’。”

SeaTunnel 的插件机制虽然灵活,但面对多点自研的 DDH 消息格式、分库分表合并、动态分区等需求,仍需我们“动手改代码”。幸运的是,SeaTunnel 的模块化设计让二次开发变得高效且可控。以下是我们重点改造的几个模块,每一项都直接解决了业务痛点。

4.1 定制DDH-Format CDC

多点自研 DDH 采集 MySQL binlog,以 Protobuf 推 Kafka。我们实现了

KafkaDeserializationSchema:

解析 Protobuf → SeaTunnelRow;

DDL 消息直接构建 CatalogTable,自动在 Paimon 侧加列;

DML 打标“before/after”,下游 StarRocks 做部分列更新。

4.2 Router Transform:多表合并+动态分区

场景:1200 张分库分表 t_order_00…t_order_1199 → 一张 paimon 表 dwd_order。

实现:

正则 t_order_(\d+) 映射目标表;

选基准 Schema(字段最多的一张表),其余表缺失字段补 NULL;

主键冲突用 $table_name + $pk 生成新 UK;

分区字段 dt 从字符串 create_time 截取,支持 yyyy-MM-dd 与 yyyyMMdd 两种格式自动识别。

配置片段:

4.3 Hive-Sink支持Overwrite

社区版只有 append,我们基于 PR #7843 二次开发:

任务提交前,先根据分区值调用 FileSystem.listStatus() 拿到旧路径;

新数据写完再原子删除旧路径,实现“幂等重跑”。

已贡献回社区,预计 2.3.14 发布。

4.4 其他补丁

JuiceFS 连接器:支持 mount 点缓存,listing 性能提升 5 倍;

Kafka-2.x 独立模块:解决 0.10/2.x 协议冲突;

升级 JDK11:Zeta 引擎 GC 时间下降 40%;

新增 JSON UDF json_extract_array/json_merge,日期 UDF date_shift(),已合入主干。

五、踩坑实录

“每一个坑,都是通往稳定的必经之路。”

开源项目再成熟,落地到真实业务场景也免不了踩坑。我们在使用 SeaTunnel 的过程中,也遇到了版本冲突、异步操作、消费延迟等问题。以下是我们踩过的几个典型“坑”,以及最终的解决方案,希望能帮你少走弯路。

问题 现象 根因 解法

S3 访问失败 Spark 3.3.4 与 SeaTunnel 默认内置 Hadoop 3.1.4 冲突 classpath 存在两份 aws-sdk 排除 Spark 的 hadoop-client,改用 SeaTunnel uber jar

StarRocks ALTER 阻塞 写入时报 “column not found” SR 的 ALTER 为异步,客户端继续写会失败 在 sink 中轮询 SHOW ALTER TABLE STATE,FINISHED 后再恢复写入

Kafka 消费慢 每秒仅 3k 条 poll 到空消息线程 sleep 100ms 提 PR #7821,支持“空轮询不 sleep”模式,吞吐量提到 12w/s

六、总结收益:三个月交卷

“技术价值,最终要用数字说话。”

我们用了 Apache SeaTunnel 不到三个月时间,完成了 3 套商家生产环境的割接。结果不仅“跑得更快”,还“跑得更省”。

Oracle、云存储、Paimon、StarRocks 等源端需求被一次性覆盖,实时同步不再靠手写 Flink;模板化“零代码”接入,让新增连接器从过去的 N 周压缩到 3 天,资源消耗仅为原 Spark 的 1/3,同样数据量跑得更轻更快。

配合全新 UI 和按需开放的数据源权限,商家 IT 自己就能配任务、看链路,交付成本骤降,使用体验直线上升,真正兑现了降本、灵活、稳态三大目标。

七、下一步:湖仓+ AI双轮驱动

package com.example.kucun2.function; import android.content.Context; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Filter; import android.widget.Filterable; import android.widget.Spinner; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.example.kucun2.entity.Bancai; import com.example.kucun2.entity.Caizhi; import com.example.kucun2.entity.Chanpin; import com.example.kucun2.entity.Dingdan; import com.example.kucun2.entity.Mupi; import com.example.kucun2.entity.Zujian; import java.util.ArrayList; import java.util.List; public class Adapter { // 安全适配器基类,处理公共逻辑 // 安全适配器基类,处理公共逻辑 private static abstract class SafeSpinnerAdapter<T> extends ArrayAdapter<T> { private static final String TAG = "SafeSpinnerAdapter"; private final LayoutInflater inflater; private final String defaultText; public SafeSpinnerAdapter(Context context, int resource, List<T> objects, String defaultText) { super(context, resource, objects); this.inflater = LayoutInflater.from(context); this.defaultText = defaultText != null ? defaultText : "请选择"; } @Override public int getCount() { return super.getCount() + 1; // 增加一个默认项 } @Override public T getItem(int position) { return position == 0 ? null : super.getItem(position - 1); } @Override public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { if (parent == null) { Log.w(TAG, "Parent is null in getDropDownView"); return new TextView(getContext()); } if (convertView == null) { convertView = inflater.inflate(android.R.layout.simple_spinner_dropdown_item, parent, false); } TextView textView = convertView.findViewById(android.R.id.text1); if (position == 0) { textView.setText(defaultText); } else { T item = getItem(position); textView.setText(item != null ? formatDropdownText(item) : ""); } return convertView; } @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { if (parent == null) { Log.w(TAG, "Parent is null in getView"); return new TextView(getContext()); } if (convertView == null) { convertView = inflater.inflate(android.R.layout.simple_spinner_item, parent, false); } TextView textView = convertView.findViewById(android.R.id.text1); if (position == 0) { textView.setText(defaultText); } else { T item = getItem(position); textView.setText(item != null ? formatDisplayText(item) : ""); } return convertView; } protected abstract String formatDisplayText(T item); protected abstract String formatDropdownText(T item); } // 1. 材质适配器 public static ArrayAdapter<Caizhi> setupCaizhiSpinner(Spinner spinner, List<Caizhi> data, Context context) { ArrayAdapter<Caizhi> adapter = new SafeSpinnerAdapter<Caizhi>( context, android.R.layout.simple_spinner_item, data, "请选择材质" ) { @Override protected String formatDisplayText(Caizhi item) { return item != null ? item.getName() : ""; } @Override protected String formatDropdownText(Caizhi item) { return item != null ? item.getName() : ""; } }; adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); spinner.setSelection(0); // 默认选中"请选择" return adapter; } // 2. 木皮适配器 public static void setupMupiSpinner(Spinner spinner, List<Mupi> data, Context context) { ArrayAdapter<Mupi> adapter = new SafeSpinnerAdapter<Mupi>( context, android.R.layout.simple_spinner_item, data, "请选择木皮" ) { @Override protected String formatDisplayText(Mupi item) { return item != null ? item.formatMupiDisplay() : ""; } @Override protected String formatDropdownText(Mupi item) { return item != null ? item.formatMupiDisplay() : ""; } }; adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); spinner.setSelection(0); // 默认选中"请选择" } // 3. 板材适配器 public static void setupBancaiSpinners(Spinner spinner, List<Bancai> data, Context context) { ArrayAdapter<Bancai> adapter = new SafeSpinnerAdapter<Bancai>( context, android.R.layout.simple_spinner_item, data, "请选择板材" ) { @Override protected String formatDisplayText(Bancai item) { return item != null ? item.TableText() : ""; } @Override protected String formatDropdownText(Bancai item) { return item != null ? item.TableText() : ""; } }; adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); spinner.setSelection(0); // 默认选中"请选择" } // 4. 订单适配器 public static void setupDingdanSpinner(Spinner spinner, List<Dingdan> data, Context context) { ArrayAdapter<Dingdan> adapter = new SafeSpinnerAdapter<Dingdan>( context, android.R.layout.simple_spinner_item, data, "请选择订单" ) { @Override protected String formatDisplayText(Dingdan item) { return item != null ? item.getNumber() : ""; } @Override protected String formatDropdownText(Dingdan item) { return item != null ? item.getNumber() : ""; } }; adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); spinner.setSelection(0); // 默认选中"请选择" } // 5. 产品适配器 public static void setupChanpinSpinner(Spinner spinner, List<Chanpin> data, Context context) { ArrayAdapter<Chanpin> adapter = new SafeSpinnerAdapter<Chanpin>( context, android.R.layout.simple_spinner_item, data, "请选择产品" ) { @Override protected String formatDisplayText(Chanpin item) { return item != null ? item.getBianhao() : ""; } @Override protected String formatDropdownText(Chanpin item) { return item != null ? item.getBianhao() : ""; } }; adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); spinner.setSelection(0); // 默认选中"请选择" } // 6. 组件适配器 public static void setupZujianSpinner(Spinner spinner, List<Zujian> data, Context context) { ArrayAdapter<Zujian> adapter = new SafeSpinnerAdapter<Zujian>( context, android.R.layout.simple_spinner_item, data, "请选择组件" ) { @Override protected String formatDisplayText(Zujian item) { return item != null ? item.getName() : ""; } @Override protected String formatDropdownText(Zujian item) { return item != null ? item.getName() : ""; } }; adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); spinner.setSelection(0); // 默认选中"请选择" } // 1. 新增支持筛选的适配器基类 /** * 新增支持筛选的适配器基类 * @param <T> */ public static abstract class FilterableAdapter<T> extends ArrayAdapter<T> implements Filterable { private static final String TAG = "FilterableAdapter"; private final LayoutInflater inflater; private List<T> originalList; private List<T> filteredList; private final String defaultText; private final ItemFilter filter = new ItemFilter(); public FilterableAdapter(Context context, int resource, List<T> objects, String defaultText) { super(context, resource, objects); this.inflater = LayoutInflater.from(context); this.originalList = new ArrayList<>(objects); this.filteredList = new ArrayList<>(objects); this.defaultText = defaultText != null ? defaultText : "请选择"; } public FilterableAdapter(Context context, int resource, List<T> objects) { super(context, resource, objects); this.inflater = LayoutInflater.from(context); this.originalList = new ArrayList<>(objects); this.filteredList = new ArrayList<>(objects); this.defaultText = "请选择"; } @Override public int getCount() { return filteredList.size() + 1; // 增加默认项 } @Override public T getItem(int position) { return position == 0 ? null : filteredList.get(position - 1); // 位置偏移处理 } @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(android.R.layout.simple_spinner_item, parent, false); } TextView textView = convertView.findViewById(android.R.id.text1); if (position == 0) { textView.setText(defaultText); // 默认提示文本 } else { T item = getItem(position); textView.setText(item != null ? formatDisplayText(item) : ""); } return convertView; } @Override public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(android.R.layout.simple_spinner_dropdown_item, parent, false); } TextView textView = convertView.findViewById(android.R.id.text1); if (position == 0) { textView.setText(defaultText); // 下拉框中的默认提示 } else { T item = getItem(position); textView.setText(item != null ? formatDropdownText(item) : ""); } return convertView; } public void updateList(List<T> newList) { this.originalList = new ArrayList<>(newList); this.filteredList = new ArrayList<>(newList); notifyDataSetChanged(); } @Override public Filter getFilter() { return filter; } // 必须实现的抽象方法 protected abstract String formatDisplayText(T item); protected abstract String formatDropdownText(T item); private class ItemFilter extends Filter { @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults results = new FilterResults(); List<T> filtered = new ArrayList<>(); if (constraint == null || constraint.length() == 0) { // 无筛选条件时包含所有项 filtered.addAll(originalList); } else { String filterPattern = constraint.toString().toLowerCase().trim(); for (T item : originalList) { if (formatDisplayText(item).toLowerCase().contains(filterPattern)) { filtered.add(item); } } } results.values = filtered; results.count = filtered.size(); return results; } @Override @SuppressWarnings("unchecked") protected void publishResults(CharSequence constraint, FilterResults results) { filteredList = (List<T>) results.values; if (results.count > 0 || constraint == null || constraint.length() == 0) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } } } } // /** * 为订单创建筛选适配器 * @param context * @param data * @return */ public static FilterableAdapter<Dingdan> createDingdanFilterableAdapter( Context context, List<Dingdan> data) { return new FilterableAdapter<Dingdan>(context, android.R.layout.simple_spinner_item, data) { @Override protected String formatDisplayText(Dingdan item) { return item != null ? item.getNumber() : ""; } @Override protected String formatDropdownText(Dingdan item) { return item != null ? item.getNumber() : ""; } }; } // 为产品创建筛选适配器 public static FilterableAdapter<Chanpin> createChanpinFilterableAdapter( Context context, List<Chanpin> data) { return new FilterableAdapter<Chanpin>(context, android.R.layout.simple_spinner_item, data) { @Override protected String formatDisplayText(Chanpin item) { return item != null ? item.getBianhao() : ""; } @Override protected String formatDropdownText(Chanpin item) { return item != null ? item.getBianhao() : ""; } }; } // 为组件创建筛选适配器 public static FilterableAdapter<Zujian> createZujianFilterableAdapter( Context context, List<Zujian> data) { return new FilterableAdapter<Zujian>(context, android.R.layout.simple_spinner_item, data) { @Override protected String formatDisplayText(Zujian item) { return item != null ? item.getName() : ""; } @Override protected String formatDropdownText(Zujian item) { return item != null ? item.getName() : ""; } }; } // 为板材创建筛选适配器 public static FilterableAdapter<Bancai> createBancaiFilterableAdapter( Context context, List<Bancai> data) { return new FilterableAdapter<Bancai>(context, android.R.layout.simple_spinner_item, data) { @Override protected String formatDisplayText(Bancai item) { return item != null ? item.TableText() : ""; } @Override protected String formatDropdownText(Bancai item) { return item != null ? item.TableText() : ""; } }; } } package com.example.kucun2.ui.jinhuo; import android.app.AlertDialog; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.Spinner; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import com.example.kucun2.DataPreserver.Data; import com.example.kucun2.R; import com.example.kucun2.entity.*; import com.example.kucun2.function.Adapter; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Objects; public class AddInventoryFragment extends Fragment implements Data.OnDataChangeListener { // 视图组件 private AutoCompleteTextView actvDingdan, actvChanpin, actvZujian, actvBancai; private EditText etQuantity; private RadioGroup rgType; private Button btnNewDingdan, btnAddChanpin, btnAddZujian, btnSubmit; // 适配器 private Adapter.FilterableAdapter<Dingdan> dingdanAdapter; private Adapter.FilterableAdapter<Chanpin> chanpinAdapter; private Adapter.FilterableAdapter<Zujian> zujianAdapter; private Adapter.FilterableAdapter<Bancai> bancaiAdapter; // 当前选择 private Dingdan selectedDingdan; private Chanpin selectedChanpin; private Zujian selectedZujian; private Bancai selectedBancai; // 数据列表 private List<Dingdan> dingdanList; private List<Chanpin> chanpinList; private List<Zujian> zujianList; private List<Bancai> bancaiList; // 当前用户 private User currentUser; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 获取当前用户 currentUser = Data.getCurrentUser(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_add_inventory, container, false); initViews(view); initData(); setupSpinners(); setupListeners(); applyPermissionRestrictions(); // 应用权限限制 return view; } /** * 初始化视图组件 */ private void initViews(View view) { actvDingdan = view.findViewById(R.id.actv_dingdan); actvChanpin = view.findViewById(R.id.actv_chanpin); actvZujian = view.findViewById(R.id.actv_zujian); actvBancai = view.findViewById(R.id.actv_bancai); etQuantity = view.findViewById(R.id.et_shuliang); rgType = view.findViewById(R.id.rg_type); btnNewDingdan = view.findViewById(R.id.btn_new_dingdan); btnAddChanpin = view.findViewById(R.id.btn_add_chanpin); btnAddZujian = view.findViewById(R.id.btn_add_zujian); btnSubmit = view.findViewById(R.id.btn_submit); // 初始禁用状态 actvChanpin.setEnabled(false); actvZujian.setEnabled(false); etQuantity.setEnabled(false); } private void initData() { // 从全局数据获取列表 dingdanList = Data.dingdans().getViewList(); chanpinList = Data.chanpins().getViewList(); zujianList = Data.zujians().getViewList(); bancaiList = Data.bancais().getViewList(); } /** * 设置下拉框适配器 */ private void setupSpinners() { // 3. 创建支持筛选的适配器 dingdanAdapter = Adapter.createDingdanFilterableAdapter(requireContext(), dingdanList); actvDingdan.setAdapter(dingdanAdapter); chanpinAdapter = Adapter.createChanpinFilterableAdapter(requireContext(), new ArrayList<>()); actvChanpin.setAdapter(chanpinAdapter); zujianAdapter = Adapter.createZujianFilterableAdapter(requireContext(), new ArrayList<>()); actvZujian.setAdapter(zujianAdapter); bancaiAdapter = Adapter.createBancaiFilterableAdapter(requireContext(), bancaiList); actvBancai.setAdapter(bancaiAdapter); } /** * 设置事件监听器 */ private void setupListeners() { // 4. 设置新的点击事件监听器 actvDingdan.setOnItemClickListener((parent, view, position, id) -> { selectedDingdan = dingdanAdapter.getItem(position); updateChanpinSpinner(); actvChanpin.setEnabled(selectedDingdan != null); if (selectedDingdan == null) { // 清后续选择 actvChanpin.setText(""); selectedChanpin = null; actvZujian.setText(""); selectedZujian = null; actvBancai.setText(""); selectedBancai = null; etQuantity.setText(""); etQuantity.setEnabled(false); } }); // 产品选择监听 actvChanpin.setOnItemClickListener((parent, view, position, id) -> { selectedChanpin = chanpinAdapter.getItem(position); updateZujianSpinner(); actvZujian.setEnabled(selectedChanpin != null); if (selectedChanpin == null) { // 清后续选择 actvZujian.setText(""); selectedZujian = null; actvBancai.setText(""); selectedBancai = null; etQuantity.setText(""); etQuantity.setEnabled(false); } }); // 组件选择监听 actvZujian.setOnItemClickListener((parent, view, position, id) -> { selectedZujian = zujianAdapter.getItem(position); updateBancaiSpinner(); // 组件选择后锁定板材下拉框 actvBancai.setEnabled(false); }); // 板材选择监听 actvBancai.setOnItemClickListener((parent, view, position, id) -> { selectedBancai = bancaiAdapter.getItem(position); etQuantity.setEnabled(selectedBancai != null); if (selectedBancai == null) { etQuantity.setText(""); } }); // 新建订单 btnNewDingdan.setOnClickListener(v -> showNewDingdanDialog()); // 添加产品 btnAddChanpin.setOnClickListener(v -> showAddChanpinDialog()); // 添加组件 btnAddZujian.setOnClickListener(v -> showAddZujianDialog()); // 提交 btnSubmit.setOnClickListener(v -> submitInventory()); } /** * 根据用户角色应用权限限制 */ private void applyPermissionRestrictions() { if (currentUser == null) return; int role = currentUser.getRole(); if (role == 0) { // 普通用户 // 只能消耗,不能进货 rgType.check(R.id.rb_xiaohao); rgType.getChildAt(0).setEnabled(false); // 禁用进货选项 // 禁用新建订单、添加产品按钮 btnNewDingdan.setEnabled(false); btnAddChanpin.setEnabled(false); } } /** * 根据选定订单更新产品下拉框 */ private void updateChanpinSpinner() { List<Chanpin> filtered = new ArrayList<>(); if (selectedDingdan != null) { for (Dingdan_chanpin dc : selectedDingdan.getDingdan_chanpin()) { filtered.add(dc.getChanpin()); } } // 5. 使用适配器的updateList方法更新数据 chanpinAdapter.updateList(filtered); } /** * 根据选定产品更新组件下拉框 */ private void updateZujianSpinner() { List<Zujian> filtered = new ArrayList<>(); if (selectedChanpin != null) { for (Chanpin_Zujian cz : selectedChanpin.getChanpin_zujian()) { filtered.add(cz.getZujian()); } } zujianAdapter.updateList(filtered); } /** * 根据选定组件更新板材下拉框 */ private void updateBancaiSpinner() { List<Bancai> filtered = new ArrayList<>(); if (selectedZujian != null && selectedChanpin != null) { // 查找组件关联的板材 for (Chanpin_Zujian cz : selectedChanpin.getChanpin_zujian()) { if (cz.getZujian().equals(selectedZujian)) { filtered.add(cz.getBancai()); // 自动选中关联的板材 selectedBancai = cz.getBancai(); actvBancai.setText(selectedBancai.TableText()); etQuantity.setEnabled(true); break; } } bancaiAdapter.updateList(filtered); } else { // 没有选择组件时显示所有板材 filtered = new ArrayList<>(bancaiList); bancaiAdapter.updateList(filtered); } } /** * 显示新建订单对话框 */ private void showNewDingdanDialog() { // 权限检查 if (currentUser != null && currentUser.getRole() == 0) { Toast.makeText(requireContext(), "您无权创建新订单", Toast.LENGTH_SHORT).show(); return; } AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); View view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_new_dingdan, null); EditText etNumber = view.findViewById(R.id.et_order_number); builder.setView(view) .setTitle("新建订单") .setPositiveButton("保存", (dialog, which) -> { Dingdan newDingdan = new Dingdan(); newDingdan.setNumber(etNumber.getText().toString()); // 添加到全局数据 Data.add(newDingdan); }) .setNegativeButton("取消", null) .show(); } /** * 显示添加产品对话框 */ private void showAddChanpinDialog() { // 权限检查 if (currentUser != null && currentUser.getRole() == 0) { Toast.makeText(requireContext(), "您无权添加产品", Toast.LENGTH_SHORT).show(); return; } if (selectedDingdan == null) { Toast.makeText(requireContext(), "请先选择订单", Toast.LENGTH_SHORT).show(); return; } AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); View view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_add_chanpin, null); Spinner spinner = view.findViewById(R.id.spinner_chanpin_selection); EditText etQuantity = view.findViewById(R.id.et_chanpin_quantity); // 设置产品列表(排除已关联的) List<Chanpin> available = new ArrayList<>(chanpinList); for (Dingdan_chanpin dc : selectedDingdan.getDingdan_chanpin()) { available.remove(dc.getChanpin()); } Adapter.setupChanpinSpinner(spinner, available, requireContext()); // 添加新建产品按钮 Button btnNewChanpin = view.findViewById(R.id.btn_new_chanpin); builder.setView(view) .setTitle("添加产品到订单") .setPositiveButton("添加", (dialog, which) -> { Chanpin selected = (Chanpin) spinner.getSelectedItem(); int quantity = Integer.parseInt(etQuantity.getText().toString().trim()); // 检查是否已存在关联 for (Dingdan_chanpin dc : selectedDingdan.getDingdan_chanpin()) { if (dc.getChanpin().equals(selected)) { Toast.makeText(requireContext(), "该产品已添加到订单", Toast.LENGTH_SHORT).show(); return; } } // 创建订单-产品关联 Dingdan_chanpin dc = new Dingdan_chanpin(); dc.setDingdan(selectedDingdan); dc.setChanpin(selected); dc.setShuliang(quantity); // 添加到全局数据 Data.add(dc); selectedDingdan.getDingdan_chanpin().add(dc); }) .show(); // 新建产品按钮点击事件 btnNewChanpin.setOnClickListener(v -> showNewChanpinDialog(available, spinner)); } // 实现新建产品对话框 private void showNewChanpinDialog(List<Chanpin> available, Spinner spinner) { AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); View view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_new_chanpin, null); EditText etBianhao = view.findViewById(R.id.et_chanpin_name); builder.setView(view) .setTitle("新建产品") .setPositiveButton("保存", (dialog, which) -> { String bianhao = etBianhao.getText().toString().trim(); if (bianhao.isEmpty()) { Toast.makeText(requireContext(), "产品编号不能为", Toast.LENGTH_SHORT).show(); return; } // 创建新产品 Chanpin newChanpin = new Chanpin(); newChanpin.setBianhao(bianhao); // 添加到全局数据 Data.add(newChanpin); // 更新可用列表和适配器 available.add(newChanpin); }) .setNegativeButton("取消", null) .show(); } /** * 显示添加组件对话框 */ private void showAddZujianDialog() { if (selectedChanpin == null) { Toast.makeText(requireContext(), "请先选择产品", Toast.LENGTH_SHORT).show(); return; } AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); View view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_create_zujian_bancai, null); Spinner spinnerZujian = view.findViewById(R.id.et_zujian_name); Spinner spinnerBancai = view.findViewById(R.id.spinner_bancai); EditText etOneHowmany = view.findViewById(R.id.number_one_howmany); // 设置组件下拉框 Adapter.setupZujianSpinner(spinnerZujian, zujianList, requireContext()); // 设置板材下拉框 Adapter.setupBancaiSpinners(spinnerBancai, bancaiList, requireContext()); builder.setView(view) .setTitle("添加组件到产品") .setPositiveButton("添加", (dialog, which) -> { Zujian zujian = (Zujian) spinnerZujian.getSelectedItem(); Bancai bancai = (Bancai) spinnerBancai.getSelectedItem(); double oneHowmany = Double.parseDouble(etOneHowmany.getText().toString()); // 检查是否已存在关联 for (Chanpin_Zujian cz : selectedChanpin.getChanpin_zujian()) { if (cz.getZujian().equals(zujian) && cz.getBancai().equals(bancai)) { Toast.makeText(requireContext(), "该组件已添加到产品", Toast.LENGTH_SHORT).show(); return; } } // 创建产品-组件关联 Chanpin_Zujian cz = new Chanpin_Zujian(); cz.setChanpin(selectedChanpin); cz.setZujian(zujian); cz.setBancai(bancai); cz.setOne_howmany(oneHowmany); zujian.getChanpin_zujian().add(cz); selectedChanpin.getChanpin_zujian().add(cz); // 添加到全局数据 Data.add(cz); }) .show(); } /** * 提交库存操作(进货/消耗) */ private void submitInventory() { // 获取数量 int quantity; try { quantity = Integer.parseInt(etQuantity.getText().toString()); } catch (NumberFormatException e) { Toast.makeText(requireContext(), "请输入有效数量", Toast.LENGTH_SHORT).show(); return; } // 获取操作类型 boolean isJinhuo = rgType.getCheckedRadioButtonId() == R.id.rb_jinhuo; // 权限检查:普通用户只能消耗 if (currentUser != null && currentUser.getRole() == 0 && isJinhuo) { Toast.makeText(requireContext(), "您只能执行生产操作", Toast.LENGTH_SHORT).show(); return; } // 创建库存记录 Jinhuo record = new Jinhuo(); record.setShuliang(isJinhuo ? quantity : -quantity); // 正数为进货,负数为消耗 record.setDate(new Date()); record.setUser(currentUser); // 设置关联关系 if (selectedBancai != null) { // 创建订单-板材关联(如果不存在) Dingdan_bancai db = createOrUpdateDingdanBancai(); record.setDingdan_bancai(db); } // 添加到全局数据 Data.add(record); Toast.makeText(requireContext(), "操作成功", Toast.LENGTH_SHORT).show(); resetForm(); } /** * 创建或更新订单-板材关联记录 */ private Dingdan_bancai createOrUpdateDingdanBancai() { // 检查是否已存在关联 Dingdan_bancai existing = findExistingDingdanBancai(); if (existing != null) { // 更新现有记录 return existing; } // 创建新关联 Dingdan_bancai db = new Dingdan_bancai(); if (selectedDingdan != null) db.setDingdan(selectedDingdan); if (selectedChanpin != null) db.setChanpin(selectedChanpin); if (selectedZujian != null) db.setZujian(selectedZujian); if (selectedBancai != null) db.setBancai(selectedBancai); // 添加到全局数据 Data.add(db); return db; } /** * 查找现有订单-板材关联记录 */ private Dingdan_bancai findExistingDingdanBancai() { for (Dingdan_bancai db : Data.Dingdan_bancais().getViewList()) { boolean matchDingdan = (selectedDingdan == null && db.getDingdan() == null) || (selectedDingdan != null && selectedDingdan.equals(db.getDingdan())); boolean matchChanpin = (selectedChanpin == null && db.getChanpin() == null) || (selectedChanpin != null && selectedChanpin.equals(db.getChanpin())); boolean matchZujian = (selectedZujian == null && db.getZujian() == null) || (selectedZujian != null && selectedZujian.equals(db.getZujian())); boolean matchBancai = selectedBancai != null && selectedBancai.equals(db.getBancai()); if (matchDingdan && matchChanpin && matchZujian && matchBancai) { return db; } } return null; } /** * 重置表单到初始状态 */ private void resetForm() { actvDingdan.setSelection(0); actvChanpin.setSelection(0); actvZujian.setSelection(0); actvBancai.setSelection(0); etQuantity.setText(""); rgType.check(R.id.rb_jinhuo); } @Override public void onResume() { super.onResume(); Data.addDataChangeListener(this); } @Override public void onPause() { super.onPause(); Data.removeDataChangeListener(this); } @Override public void onDataChanged(Class<?> entityClass, String operationType, Integer itemId) { // 6. 更新适配器数据 if (entityClass == Dingdan.class) { dingdanList = Data.dingdans().getViewList(); dingdanAdapter.updateList(dingdanList); // 尝试选中新添加的订单 if (operationType.equals("add")) { for (int i = 0; i < dingdanList.size(); i++) { if (Objects.equals(dingdanList.get(i).getId(), itemId)) { actvDingdan.setText(dingdanList.get(i).getNumber(), false); selectedDingdan = dingdanList.get(i); break; } } } } else if (entityClass == Chanpin.class) { chanpinList = Data.chanpins().getViewList(); updateChanpinSpinner(); } else if (entityClass == Zujian.class) { zujianList = Data.zujians().getViewList(); updateZujianSpinner(); } else if (entityClass == Bancai.class) { bancaiList = Data.bancais().getViewList(); bancaiAdapter.updateList(bancaiList); } } } ---------------------------------------------------AutoCompleteTextView中输入时现实的下拉框是订单编号,选择之后就显示为内存地址了
07-01
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值