You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 03:06:12