Django如何实现支持嵌套字段选择的票据打印模板构造器
Django自定义票据打印模板实现方案
你提出的新增实体存储模板配置的思路完全可行,是这类无代码配置需求的标准实现路径,整套逻辑不需要侵入原有业务代码,完全满足管理员后台可视化配置、跨关联取字段的要求,具体实现步骤如下:
1. 新增模板存储模型
需要两张表分别存储模板基础信息、模板关联的字段配置,字段路径直接复用Django原生的双下划线跨表关联语法,不需要额外开发路径解析逻辑:
from django.db import models # 补全原有模型缺失的关联:如果需要展示承运商,给Itinerary加如下外键即可 # class Itinerary(models.Model): # ... 原有字段 ... # carrier = models.ForeignKey(Carrier, on_delete=models.CASCADE, related_name="itineraries", verbose_name="承运商") class PrintTemplate(models.Model): """票据打印模板主表""" name = models.CharField(max_length=64, unique=True, verbose_name="模板名称") # 如"常规模板"、"精简模板" description = models.TextField(blank=True, verbose_name="模板说明") is_default = models.BooleanField(default=False, verbose_name="是否为默认模板") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "打印模板" verbose_name_plural = verbose_name class TemplateField(models.Model): """模板下的字段配置""" template = models.ForeignKey(PrintTemplate, on_delete=models.CASCADE, related_name="fields", verbose_name="所属模板") # 存储双下划线格式的字段路径,支持跨表,如 passengers_name、voyage__departure_date、voyage__itinerary__carrier__name field_path = models.CharField(max_length=255, verbose_name="字段路径") # 管理员自定义的字段显示名,如"出发时间"、"座位号" display_label = models.CharField(max_length=64, verbose_name="显示名称") sort_order = models.PositiveIntegerField(default=0, verbose_name="排序权重") # 可选扩展样式配置 is_bold = models.BooleanField(default=False, verbose_name="是否加粗显示") class Meta: verbose_name = "模板字段" verbose_name_plural = verbose_name ordering = ["sort_order"] unique_together = [("template", "field_path")]
2. 实现可选字段自动扫描
写一个递归工具函数,自动遍历Ticket模型及所有关联模型的字段,生成可选字段列表,不需要手动维护字段清单,后续模型加字段会自动同步到配置选项里:
def get_available_ticket_fields(prefix="", model=None, depth=0, max_depth=4): """递归获取Ticket及关联模型的所有可展示字段,控制最大递归深度避免循环引用""" if model is None: model = Ticket fields = [] if depth > max_depth: return fields for field in model._meta.get_fields(): # 排除自动生成字段、反向关联、多对多字段这类不适合展示的内容 if field.auto_created or field.one_to_many or field.many_to_many: continue current_path = f"{prefix}{field.name}" if prefix else field.name # 外键/一对一字段递归扫描关联表字段 if field.many_to_one or field.one_to_one: fields.extend(get_available_ticket_fields( prefix=f"{current_path}__", model=field.related_model, depth=depth+1, max_depth=max_depth )) else: # 取字段定义的verbose_name作为默认显示名 default_label = field.verbose_name if hasattr(field, "verbose_name") else field.name fields.append({ "path": current_path, "label": default_label }) return fields
调用get_available_ticket_fields()即可拿到所有支持选择的字段,包括跨多层关联的嵌套字段,直接作为配置页的下拉选项即可。
3. 搭建模板配置后台
- 快速实现可以直接集成Django Admin:给PrintTemplate注册Admin类,将TemplateField作为内联表单嵌入,字段路径的下拉选项用上面工具函数返回的结果做choices,管理员可以直接新增模板、调整字段排序、修改显示名称。
- 如果需要独立的配置页面,写对应视图将可选字段传给前端,用穿梭框/拖拽组件做字段添加、排序的交互,保存时将选中的字段按顺序写入TemplateField表即可。
4. 实现票据渲染逻辑
写通用工具函数,传入Ticket实例和模板对象,自动读取配置、跨表取数、生成渲染上下文,不需要针对每个模板单独写查询逻辑:
def get_ticket_render_data(ticket: Ticket, template: PrintTemplate): """根据模板配置生成票据渲染所需的结构化数据""" field_configs = list(template.fields.all()) field_paths = [config.field_path for config in field_configs] # 利用Django ORM的双下划线语法一次性取出所有需要的字段,自动处理跨表关联 ticket_values = Ticket.objects.filter(pk=ticket.pk).values(*field_paths).first() render_fields = [] for config in field_configs: value = ticket_values.get(config.field_path) # 自动处理choice字段的显示值 first_field_name = config.field_path.split("__")[0] first_field = Ticket._meta.get_field(first_field_name) if hasattr(first_field, "choices") and len(config.field_path.split("__")) == 1: value = dict(first_field.choices).get(value, value) # 可在这里统一加时间格式化、字段值转义等通用处理 render_fields.append({ "label": config.display_label, "value": value, "is_bold": config.is_bold }) return { "template_name": template.name, "fields": render_fields, "ticket": ticket }
在原有创建票据的视图中,保存ticket后读取用户选中的模板(或默认模板),调用上述函数拿到渲染数据,传入打印模板页面即可。打印页只需要做通用的字段循环渲染,加好打印适配的CSS样式,就可以直接调用浏览器打印功能输出票据。
可选优化点
- 模板配置变更频率低,可以将模板字段配置做缓存,减少打印时的数据库查询。
- 增加模板预览功能,管理员配置完字段后可以选择已有票据实时预览渲染效果,不需要实际出票测试。
- TemplateField可以扩展更多样式配置项,比如字号、对齐方式、是否占满整行,满足更复杂的排版需求。
内容的提问来源于stack exchange,提问作者Echo Foe
相关产品推荐
相关产品推荐

