Ver código fonte

收银系统 票据样式开发

liudongzhi 2 semanas atrás
pai
commit
199cc024a1

+ 73 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreReceiptTemplateConfig.java

@@ -0,0 +1,73 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@JsonInclude
+@TableName("store_receipt_template_config")
+@ApiModel(value = "StoreReceiptTemplateConfig对象", description = "店铺票据模板配置")
+public class StoreReceiptTemplateConfig {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "票据类型(1:客单,2:结账单)")
+    @TableField("receipt_type")
+    private Integer receiptType;
+
+    @ApiModelProperty(value = "模板类型(1:默认模板,2:自定义模板)")
+    @TableField("template_type")
+    private Integer templateType;
+
+    @ApiModelProperty(value = "模板名称")
+    @TableField("template_name")
+    private String templateName;
+
+    @ApiModelProperty(value = "是否启用(0:否,1:是)")
+    @TableField("enabled")
+    private Integer enabled;
+
+    @ApiModelProperty(value = "模板配置JSON")
+    @TableField("template_config_json")
+    private String templateConfigJson;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 19 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreReceiptTemplateCreateDTO.java

@@ -0,0 +1,19 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(value = "StoreReceiptTemplateCreateDTO对象", description = "创建票据模板参数")
+public class StoreReceiptTemplateCreateDTO {
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "票据类型(1:客单,2:结账单)", required = true)
+    private Integer receiptType;
+
+    @ApiModelProperty(value = "模板名称", required = true)
+    private String templateName;
+}

+ 16 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreReceiptTemplateDeleteDTO.java

@@ -0,0 +1,16 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(value = "StoreReceiptTemplateDeleteDTO对象", description = "按ID删除票据模板参数")
+public class StoreReceiptTemplateDeleteDTO {
+
+    @ApiModelProperty(value = "模板ID", required = true)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID(可选,用于所有权校验)")
+    private Integer storeId;
+}

+ 31 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreReceiptTemplateEditDTO.java

@@ -0,0 +1,31 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(value = "StoreReceiptTemplateEditDTO对象", description = "按ID编辑票据模板参数")
+public class StoreReceiptTemplateEditDTO {
+
+    @ApiModelProperty(value = "模板ID", required = true)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "票据类型(1:客单,2:结账单)", required = true)
+    private Integer receiptType;
+
+    @ApiModelProperty(value = "模板类型(1:默认模板,2:自定义模板)", required = true)
+    private Integer templateType;
+
+    @ApiModelProperty(value = "模板名称")
+    private String templateName;
+
+    @ApiModelProperty(value = "是否启用(0:否,1:是)")
+    private Integer enabled;
+
+    @ApiModelProperty(value = "模板配置JSON", required = true)
+    private String templateConfigJson;
+}

+ 19 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreReceiptTemplateResetDTO.java

@@ -0,0 +1,19 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(value = "StoreReceiptTemplateResetDTO对象", description = "恢复票据模板默认参数")
+public class StoreReceiptTemplateResetDTO {
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "票据类型(1:客单,2:结账单)", required = true)
+    private Integer receiptType;
+
+    @ApiModelProperty(value = "模板类型(1:默认模板,2:自定义模板)", required = true)
+    private Integer templateType;
+}

+ 28 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreReceiptTemplateSaveDTO.java

@@ -0,0 +1,28 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(value = "StoreReceiptTemplateSaveDTO对象", description = "保存票据模板配置参数")
+public class StoreReceiptTemplateSaveDTO {
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty(value = "票据类型(1:客单,2:结账单)", required = true)
+    private Integer receiptType;
+
+    @ApiModelProperty(value = "模板类型(1:默认模板,2:自定义模板)", required = true)
+    private Integer templateType;
+
+    @ApiModelProperty(value = "模板名称")
+    private String templateName;
+
+    @ApiModelProperty(value = "是否启用(0:否,1:是)")
+    private Integer enabled;
+
+    @ApiModelProperty(value = "模板配置JSON", required = true)
+    private String templateConfigJson;
+}

+ 7 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreReceiptTemplateConfigMapper.java

@@ -0,0 +1,7 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.StoreReceiptTemplateConfig;
+
+public interface StoreReceiptTemplateConfigMapper extends BaseMapper<StoreReceiptTemplateConfig> {
+}

+ 126 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreReceiptTemplateConfigController.java

@@ -0,0 +1,126 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreReceiptTemplateConfig;
+import shop.alien.entity.store.dto.StoreReceiptTemplateCreateDTO;
+import shop.alien.entity.store.dto.StoreReceiptTemplateEditDTO;
+import shop.alien.entity.store.dto.StoreReceiptTemplateResetDTO;
+import shop.alien.entity.store.dto.StoreReceiptTemplateDeleteDTO;
+import shop.alien.entity.store.dto.StoreReceiptTemplateSaveDTO;
+import shop.alien.store.service.StoreReceiptTemplateConfigService;
+
+import java.util.List;
+
+@Slf4j
+@Api(tags = {"票据样式模板"})
+@CrossOrigin
+@RestController
+@RequestMapping("/store/receipt/template")
+@RequiredArgsConstructor
+public class StoreReceiptTemplateConfigController {
+
+    private final StoreReceiptTemplateConfigService storeReceiptTemplateConfigService;
+
+    @ApiOperation("查询模板列表(按门店+票据类型)")
+    @ApiOperationSupport(order = 1)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "receiptType", value = "票据类型(1:客单,2:结账单)", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/list")
+    public R<List<StoreReceiptTemplateConfig>> list(@RequestParam Integer storeId, @RequestParam Integer receiptType) {
+        log.info("StoreReceiptTemplateConfigController.list?storeId={},receiptType={}", storeId, receiptType);
+        try {
+            return R.data(storeReceiptTemplateConfigService.listByStoreAndReceiptType(storeId, receiptType));
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("查询模板详情")
+    @ApiOperationSupport(order = 2)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "receiptType", value = "票据类型(1:客单,2:结账单)", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "templateType", value = "模板类型(1:默认模板,2:自定义模板)", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/detail")
+    public R<StoreReceiptTemplateConfig> detail(@RequestParam Integer storeId,
+                                                @RequestParam Integer receiptType,
+                                                @RequestParam Integer templateType) {
+        log.info("StoreReceiptTemplateConfigController.detail?storeId={},receiptType={},templateType={}", storeId, receiptType, templateType);
+        try {
+            return R.data(storeReceiptTemplateConfigService.getDetail(storeId, receiptType, templateType));
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("保存模板配置")
+    @ApiOperationSupport(order = 3)
+    @PostMapping("/save")
+    public R<StoreReceiptTemplateConfig> save(@RequestBody StoreReceiptTemplateSaveDTO dto) {
+        log.info("StoreReceiptTemplateConfigController.save?dto={}", dto);
+        try {
+            return R.data(storeReceiptTemplateConfigService.saveTemplate(dto));
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+    }
+
+//    @ApiOperation("创建自定义模板")
+//    @ApiOperationSupport(order = 4)
+//    @PostMapping("/create")
+//    public R<StoreReceiptTemplateConfig> create(@RequestBody StoreReceiptTemplateCreateDTO dto) {
+//        log.info("StoreReceiptTemplateConfigController.create?dto={}", dto);
+//        try {
+//            return R.data(storeReceiptTemplateConfigService.createTemplate(dto));
+//        } catch (Exception e) {
+//            return R.fail(e.getMessage());
+//        }
+//    }
+
+    @ApiOperation("恢复默认模板")
+    @ApiOperationSupport(order = 5)
+    @PostMapping("/resetDefault")
+    public R<StoreReceiptTemplateConfig> resetDefault(@RequestBody StoreReceiptTemplateResetDTO dto) {
+        log.info("StoreReceiptTemplateConfigController.resetDefault?dto={}", dto);
+        try {
+            return R.data(storeReceiptTemplateConfigService.resetToDefault(dto.getStoreId(), dto.getReceiptType(), dto.getTemplateType()));
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("按ID编辑模板")
+    @ApiOperationSupport(order = 6)
+    @PostMapping("/update")
+    public R<StoreReceiptTemplateConfig> update(@RequestBody StoreReceiptTemplateEditDTO dto) {
+        log.info("StoreReceiptTemplateConfigController.update?dto={}", dto);
+        try {
+            return R.data(storeReceiptTemplateConfigService.updateTemplateById(dto));
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("按ID删除模板")
+    @ApiOperationSupport(order = 7)
+    @PostMapping("/delete")
+    public R<Boolean> delete(@RequestBody StoreReceiptTemplateDeleteDTO dto) {
+        log.info("StoreReceiptTemplateConfigController.delete?dto={}", dto);
+        try {
+            return R.data(storeReceiptTemplateConfigService.deleteTemplateById(dto));
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+    }
+}

+ 26 - 0
alien-store/src/main/java/shop/alien/store/service/StoreReceiptTemplateConfigService.java

@@ -0,0 +1,26 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.StoreReceiptTemplateConfig;
+import shop.alien.entity.store.dto.StoreReceiptTemplateCreateDTO;
+import shop.alien.entity.store.dto.StoreReceiptTemplateEditDTO;
+import shop.alien.entity.store.dto.StoreReceiptTemplateDeleteDTO;
+import shop.alien.entity.store.dto.StoreReceiptTemplateSaveDTO;
+
+import java.util.List;
+
+public interface StoreReceiptTemplateConfigService {
+
+    List<StoreReceiptTemplateConfig> listByStoreAndReceiptType(Integer storeId, Integer receiptType);
+
+    StoreReceiptTemplateConfig getDetail(Integer storeId, Integer receiptType, Integer templateType);
+
+//    StoreReceiptTemplateConfig createTemplate(StoreReceiptTemplateCreateDTO dto);
+
+    StoreReceiptTemplateConfig saveTemplate(StoreReceiptTemplateSaveDTO dto);
+
+    StoreReceiptTemplateConfig resetToDefault(Integer storeId, Integer receiptType, Integer templateType);
+
+    StoreReceiptTemplateConfig updateTemplateById(StoreReceiptTemplateEditDTO dto);
+
+    Boolean deleteTemplateById(StoreReceiptTemplateDeleteDTO dto);
+}

+ 452 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreReceiptTemplateConfigServiceImpl.java

@@ -0,0 +1,452 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.StoreReceiptTemplateConfig;
+import shop.alien.entity.store.dto.StoreReceiptTemplateCreateDTO;
+import shop.alien.entity.store.dto.StoreReceiptTemplateEditDTO;
+import shop.alien.entity.store.dto.StoreReceiptTemplateDeleteDTO;
+import shop.alien.entity.store.dto.StoreReceiptTemplateSaveDTO;
+import shop.alien.mapper.StoreReceiptTemplateConfigMapper;
+import shop.alien.store.service.StoreReceiptTemplateConfigService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class StoreReceiptTemplateConfigServiceImpl implements StoreReceiptTemplateConfigService {
+
+    private final StoreReceiptTemplateConfigMapper storeReceiptTemplateConfigMapper;
+
+    private static final Set<String> SUPPORTED_FIELD_KEYS = new HashSet<>(Arrays.asList(
+            "ticketName", "storeName", "orderNo", "tableNo", "peopleCount", "diningTime", "cashierTime", "name", "phone",
+            "customData", "beginRemark",
+            "item", "wholeRemark", "countSubtotal", "orderPrice",
+            "paymentDiscount", "paymentInfo",
+            "orderBy", "orderAt", "printBy", "printAt", "customImage", "customText", "storeAddress", "storeTel"
+    ));
+
+    private static final Set<String> SUPPORTED_SUB_KEYS = new HashSet<>(Arrays.asList(
+            "unitPrice", "quantity", "subtotal",
+            "dishPriceTotal", "serviceFeeTotal", "otherServiceFeeTotal", "chargeReason", "orderTotal",
+            "coupon", "manualReduction", "reductionReason",
+            "settlementMethod", "paymentMethod", "paymentTotal", "divider"
+    ));
+
+    @Override
+    public List<StoreReceiptTemplateConfig> listByStoreAndReceiptType(Integer storeId, Integer receiptType) {
+        validateStoreAndReceiptType(storeId, receiptType);
+        ensureDefaultTemplate(storeId, receiptType);
+        LambdaQueryWrapper<StoreReceiptTemplateConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreReceiptTemplateConfig::getStoreId, storeId)
+                .eq(StoreReceiptTemplateConfig::getReceiptType, receiptType)
+                .eq(StoreReceiptTemplateConfig::getDeleteFlag, 0)
+                .orderByAsc(StoreReceiptTemplateConfig::getTemplateType, StoreReceiptTemplateConfig::getId);
+        return storeReceiptTemplateConfigMapper.selectList(wrapper);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteTemplateById(StoreReceiptTemplateDeleteDTO dto) {
+        if (dto == null || dto.getId() == null || dto.getId() <= 0) {
+            throw new RuntimeException("模板ID不能为空");
+        }
+        StoreReceiptTemplateConfig existing = storeReceiptTemplateConfigMapper.selectById(dto.getId());
+        if (existing == null || (existing.getDeleteFlag() != null && existing.getDeleteFlag() == 1)) {
+            return true; // 视为已删除
+        }
+        if (dto.getStoreId() != null && !dto.getStoreId().equals(existing.getStoreId())) {
+            throw new RuntimeException("模板归属不匹配,禁止删除");
+        }
+        if (existing.getTemplateType() != null && existing.getTemplateType() == 1) {
+            throw new RuntimeException("默认模板不可删除");
+        }
+
+
+        LambdaUpdateWrapper<StoreReceiptTemplateConfig> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(StoreReceiptTemplateConfig::getId, dto.getId());
+        wrapper.set(StoreReceiptTemplateConfig::getDeleteFlag, 1);
+        wrapper.set(StoreReceiptTemplateConfig::getUpdatedTime, new Date());
+        return storeReceiptTemplateConfigMapper.update(null, wrapper) > 0;
+    }
+
+    @Override
+    public StoreReceiptTemplateConfig getDetail(Integer storeId, Integer receiptType, Integer templateType) {
+        validateStoreAndReceiptType(storeId, receiptType);
+        validateTemplateType(templateType);
+        ensureDefaultTemplate(storeId, receiptType);
+        return getOne(storeId, receiptType, templateType);
+    }
+
+//    @Override
+//    @Transactional(rollbackFor = Exception.class)
+//    public StoreReceiptTemplateConfig createTemplate(StoreReceiptTemplateCreateDTO dto) {
+//        if (dto == null) {
+//            throw new RuntimeException("参数不能为空");
+//        }
+//        validateStoreAndReceiptType(dto.getStoreId(), dto.getReceiptType());
+//        if (StringUtils.isBlank(dto.getTemplateName())) {
+//            throw new RuntimeException("模板名称不能为空");
+//        }
+//        ensureDefaultTemplate(dto.getStoreId(), dto.getReceiptType());
+//
+//        // 创建与编辑保存走同一套保存逻辑;新建时用默认模板配置初始化自定义模板
+//        StoreReceiptTemplateConfig defaultTemplate = getOne(dto.getStoreId(), dto.getReceiptType(), 1);
+//        String initConfig = defaultTemplate != null ? defaultTemplate.getTemplateConfigJson() : buildDefaultTemplateJson();
+//
+//        StoreReceiptTemplateSaveDTO saveDTO = new StoreReceiptTemplateSaveDTO();
+//        saveDTO.setStoreId(dto.getStoreId());
+//        saveDTO.setReceiptType(dto.getReceiptType());
+//        saveDTO.setTemplateType(2);
+//        saveDTO.setTemplateName(dto.getTemplateName().trim());
+//        saveDTO.setEnabled(0);
+//        saveDTO.setTemplateConfigJson(initConfig);
+//        return saveTemplate(saveDTO);
+//    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public StoreReceiptTemplateConfig saveTemplate(StoreReceiptTemplateSaveDTO dto) {
+        validateSaveDto(dto);
+        validateTemplateConfigJson(dto.getTemplateConfigJson());
+        ensureDefaultTemplate(dto.getStoreId(), dto.getReceiptType());
+
+        StoreReceiptTemplateConfig existing = getOne(dto.getStoreId(), dto.getReceiptType(), dto.getTemplateType());
+        Integer userId = getCurrentUserId();
+        if (existing == null) {
+            existing = new StoreReceiptTemplateConfig();
+            existing.setStoreId(dto.getStoreId());
+            existing.setReceiptType(dto.getReceiptType());
+            existing.setTemplateType(dto.getTemplateType());
+            existing.setTemplateName(defaultTemplateName(dto.getTemplateType()));
+            existing.setEnabled(dto.getTemplateType() == 1 ? 1 : 0);
+            existing.setTemplateConfigJson(buildDefaultTemplateJson());
+            existing.setDeleteFlag(0);
+            existing.setCreatedTime(new Date());
+            existing.setCreatedUserId(userId);
+            storeReceiptTemplateConfigMapper.insert(existing);
+        }
+
+        if (StringUtils.isNotBlank(dto.getTemplateName())) {
+            existing.setTemplateName(dto.getTemplateName().trim());
+        }
+        if (dto.getEnabled() != null) {
+            existing.setEnabled(dto.getEnabled() == 1 ? 1 : 0);
+        }
+        existing.setTemplateConfigJson(dto.getTemplateConfigJson());
+        existing.setUpdatedTime(new Date());
+        existing.setUpdatedUserId(userId);
+        storeReceiptTemplateConfigMapper.updateById(existing);
+
+        if (existing.getEnabled() != null && existing.getEnabled() == 1) {
+            // 一个类型只允许一个启用模板
+            LambdaUpdateWrapper<StoreReceiptTemplateConfig> updateWrapper = new LambdaUpdateWrapper<>();
+            updateWrapper.eq(StoreReceiptTemplateConfig::getStoreId, existing.getStoreId())
+                    .eq(StoreReceiptTemplateConfig::getReceiptType, existing.getReceiptType())
+                    .ne(StoreReceiptTemplateConfig::getId, existing.getId())
+                    .set(StoreReceiptTemplateConfig::getEnabled, 0);
+            storeReceiptTemplateConfigMapper.update(null, updateWrapper);
+        }
+        return getOne(existing.getStoreId(), existing.getReceiptType(), existing.getTemplateType());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public StoreReceiptTemplateConfig resetToDefault(Integer storeId, Integer receiptType, Integer templateType) {
+        validateStoreAndReceiptType(storeId, receiptType);
+        validateTemplateType(templateType);
+        ensureDefaultTemplate(storeId, receiptType);
+        StoreReceiptTemplateConfig existing = getOne(storeId, receiptType, templateType);
+        if (existing == null) {
+            throw new RuntimeException("模板不存在");
+        }
+        existing.setTemplateConfigJson(buildDefaultTemplateJson());
+        existing.setUpdatedTime(new Date());
+        existing.setUpdatedUserId(getCurrentUserId());
+        storeReceiptTemplateConfigMapper.updateById(existing);
+        return existing;
+    }
+
+    private void ensureDefaultTemplate(Integer storeId, Integer receiptType) {
+        StoreReceiptTemplateConfig defaultTemplate = getOne(storeId, receiptType, 1);
+        if (defaultTemplate == null) {
+            createDefault(storeId, receiptType, 1, "默认模板", 1);
+        }
+    }
+
+    private void createDefault(Integer storeId, Integer receiptType, Integer templateType, String templateName, Integer enabled) {
+        StoreReceiptTemplateConfig config = new StoreReceiptTemplateConfig();
+        config.setStoreId(storeId);
+        config.setReceiptType(receiptType);
+        config.setTemplateType(templateType);
+        config.setTemplateName(templateName);
+        config.setEnabled(enabled);
+        config.setTemplateConfigJson(buildDefaultTemplateJson());
+        config.setDeleteFlag(0);
+        config.setCreatedTime(new Date());
+        config.setCreatedUserId(getCurrentUserId());
+        storeReceiptTemplateConfigMapper.insert(config);
+    }
+
+    private StoreReceiptTemplateConfig getOne(Integer storeId, Integer receiptType, Integer templateType) {
+        LambdaQueryWrapper<StoreReceiptTemplateConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreReceiptTemplateConfig::getStoreId, storeId)
+                .eq(StoreReceiptTemplateConfig::getReceiptType, receiptType)
+                .eq(StoreReceiptTemplateConfig::getTemplateType, templateType)
+                .eq(StoreReceiptTemplateConfig::getDeleteFlag, 0)
+                .last("limit 1");
+        return storeReceiptTemplateConfigMapper.selectOne(wrapper);
+    }
+
+    private void validateSaveDto(StoreReceiptTemplateSaveDTO dto) {
+        if (dto == null) {
+            throw new RuntimeException("参数不能为空");
+        }
+        validateStoreAndReceiptType(dto.getStoreId(), dto.getReceiptType());
+        validateTemplateType(dto.getTemplateType());
+        if (StringUtils.isBlank(dto.getTemplateConfigJson())) {
+            throw new RuntimeException("模板配置不能为空");
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public StoreReceiptTemplateConfig updateTemplateById(StoreReceiptTemplateEditDTO dto) {
+        if (dto == null || dto.getId() == null || dto.getId() <= 0) {
+            throw new RuntimeException("模板ID不能为空");
+        }
+        validateStoreAndReceiptType(dto.getStoreId(), dto.getReceiptType());
+        validateTemplateType(dto.getTemplateType());
+        validateTemplateConfigJson(dto.getTemplateConfigJson());
+
+        StoreReceiptTemplateConfig existing = storeReceiptTemplateConfigMapper.selectById(dto.getId());
+        if (existing == null || existing.getDeleteFlag() != null && existing.getDeleteFlag() == 1) {
+            throw new RuntimeException("模板不存在或已删除");
+        }
+        if (!existing.getStoreId().equals(dto.getStoreId())
+                || !existing.getReceiptType().equals(dto.getReceiptType())
+                || !existing.getTemplateType().equals(dto.getTemplateType())) {
+            throw new RuntimeException("模板归属不匹配,禁止跨店铺或跨类型修改");
+        }
+
+        if (StringUtils.isNotBlank(dto.getTemplateName())) {
+            existing.setTemplateName(dto.getTemplateName().trim());
+        }
+        if (dto.getEnabled() != null) {
+            existing.setEnabled(dto.getEnabled() == 1 ? 1 : 0);
+        }
+        existing.setTemplateConfigJson(dto.getTemplateConfigJson());
+        existing.setUpdatedTime(new Date());
+        existing.setUpdatedUserId(getCurrentUserId());
+        storeReceiptTemplateConfigMapper.updateById(existing);
+
+        if (existing.getEnabled() != null && existing.getEnabled() == 1) {
+            LambdaUpdateWrapper<StoreReceiptTemplateConfig> updateWrapper = new LambdaUpdateWrapper<>();
+            updateWrapper.eq(StoreReceiptTemplateConfig::getStoreId, existing.getStoreId())
+                    .eq(StoreReceiptTemplateConfig::getReceiptType, existing.getReceiptType())
+                    .ne(StoreReceiptTemplateConfig::getId, existing.getId())
+                    .set(StoreReceiptTemplateConfig::getEnabled, 0);
+            storeReceiptTemplateConfigMapper.update(null, updateWrapper);
+        }
+        return existing;
+    }
+
+    private void validateStoreAndReceiptType(Integer storeId, Integer receiptType) {
+        if (storeId == null || storeId <= 0) {
+            throw new RuntimeException("门店ID不能为空且必须大于0");
+        }
+        if (receiptType == null || (receiptType != 1 && receiptType != 2)) {
+            throw new RuntimeException("票据类型仅支持 1-客单, 2-结账单");
+        }
+    }
+
+    private void validateTemplateType(Integer templateType) {
+        if (templateType == null || (templateType != 1 && templateType != 2)) {
+            throw new RuntimeException("模板类型仅支持 1-默认模板, 2-自定义模板");
+        }
+    }
+
+    private String defaultTemplateName(Integer templateType) {
+        return templateType != null && templateType == 2 ? "自定义模板" : "默认模板";
+    }
+
+    private void validateTemplateConfigJson(String json) {
+        JSONObject root;
+        try {
+            root = JSON.parseObject(json);
+        } catch (Exception e) {
+            throw new RuntimeException("模板配置JSON格式不正确");
+        }
+        if (root == null) {
+            throw new RuntimeException("模板配置不能为空");
+        }
+        JSONArray sections = root.getJSONArray("sections");
+        if (sections == null) {
+            throw new RuntimeException("模板配置缺少sections");
+        }
+        for (int i = 0; i < sections.size(); i++) {
+            JSONObject section = sections.getJSONObject(i);
+            if (section == null) {
+                continue;
+            }
+            JSONArray fields = section.getJSONArray("fields");
+            if (fields == null) {
+                continue;
+            }
+            for (int j = 0; j < fields.size(); j++) {
+                JSONObject field = fields.getJSONObject(j);
+                if (field == null) {
+                    continue;
+                }
+                String key = field.getString("key");
+                if (StringUtils.isBlank(key) || !SUPPORTED_FIELD_KEYS.contains(key)) {
+                    throw new RuntimeException("包含不支持的字段: " + key);
+                }
+                JSONArray children = field.getJSONArray("children");
+                if (children == null) {
+                    continue;
+                }
+                for (int k = 0; k < children.size(); k++) {
+                    JSONObject child = children.getJSONObject(k);
+                    if (child == null) {
+                        continue;
+                    }
+                    String subKey = child.getString("key");
+                    if (StringUtils.isBlank(subKey) || !SUPPORTED_SUB_KEYS.contains(subKey)) {
+                        throw new RuntimeException("包含不支持的子字段: " + subKey);
+                    }
+                }
+            }
+        }
+    }
+
+    private Integer getCurrentUserId() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                return userInfo.getInteger("userId");
+            }
+        } catch (Exception e) {
+            log.warn("获取当前登录用户ID失败: {}", e.getMessage());
+        }
+        return null;
+    }
+
+    private String buildDefaultTemplateJson() {
+        JSONObject root = new JSONObject();
+        JSONArray sections = new JSONArray();
+        root.put("sections", sections);
+
+        sections.add(createSection("basic", "基础信息", createBasicFields()));
+        sections.add(createSection("itemInfo", "品相信息", createItemFields()));
+        sections.add(createSection("settlement", "结算信息", createSettlementFields()));
+        sections.add(createSection("footer", "底栏", createFooterFields()));
+        return root.toJSONString();
+    }
+
+    private JSONObject createSection(String key, String name, JSONArray fields) {
+        JSONObject section = new JSONObject();
+        section.put("key", key);
+        section.put("name", name);
+        section.put("fields", fields);
+        return section;
+    }
+
+    private JSONArray createBasicFields() {
+        JSONArray fields = new JSONArray();
+        fields.add(createTextField("ticketName", "票据名称", true));
+        fields.add(createTextField("storeName", "店铺名称", true));
+        fields.add(createTextField("orderNo", "单号", true));
+        fields.add(createTextField("tableNo", "桌号", true));
+        fields.add(createTextField("peopleCount", "人数", true));
+        fields.add(createTextField("diningTime", "就餐时间", true));
+        fields.add(createTextField("cashierTime", "结账时间", true));
+        fields.add(createTextField("name", "姓名", true));
+        fields.add(createTextField("phone", "电话", true));
+        fields.add(createTextField("customData", "自定义数据", false));
+        fields.add(createTextField("beginRemark", "开台备注", false));
+        return fields;
+    }
+
+    private JSONArray createItemFields() {
+        JSONArray fields = new JSONArray();
+        fields.add(createSwitchField("item", "品项", true,
+                new String[]{"unitPrice", "quantity", "subtotal"},
+                new String[]{"单价", "数量", "小计"},
+                new boolean[]{true, true, true}));
+        fields.add(createTextField("wholeRemark", "整单备注", false));
+        fields.add(createTextField("countSubtotal", "数量合计", false));
+        fields.add(createSwitchField("orderPrice", "订单价格", true,
+                new String[]{"dishPriceTotal", "serviceFeeTotal", "otherServiceFeeTotal", "chargeReason", "orderTotal"},
+                new String[]{"菜品价格合计", "服务费合计", "其他费用合计", "收费原因", "订单合计"},
+                new boolean[]{true, true, false, false, true}));
+        return fields;
+    }
+
+    private JSONArray createSettlementFields() {
+        JSONArray fields = new JSONArray();
+        fields.add(createSwitchField("paymentDiscount", "支付优惠", true,
+                new String[]{"coupon", "manualReduction", "reductionReason"},
+                new String[]{"优惠券", "手动减免", "减免原因"},
+                new boolean[]{true, false, false}));
+        fields.add(createSwitchField("paymentInfo", "支付信息", true,
+                new String[]{"settlementMethod", "paymentMethod", "paymentTotal", "divider"},
+                new String[]{"结算方式", "支付方式", "支付合计", "分割线"},
+                new boolean[]{true, true, true, true}));
+        return fields;
+    }
+
+    private JSONArray createFooterFields() {
+        JSONArray fields = new JSONArray();
+        fields.add(createTextField("orderBy", "下单人", true));
+        fields.add(createTextField("orderAt", "下单时间", true));
+        fields.add(createTextField("printBy", "打印人", true));
+        fields.add(createTextField("printAt", "打印时间", true));
+        fields.add(createTextField("customImage", "自定义图片", false));
+        fields.add(createTextField("customText", "自定义文字", false));
+        fields.add(createTextField("storeAddress", "门店地址", false));
+        fields.add(createTextField("storeTel", "门店电话", false));
+        return fields;
+    }
+
+    private JSONObject createTextField(String key, String titleName, boolean visible) {
+        JSONObject field = new JSONObject();
+        field.put("key", key);
+        field.put("titleName", titleName);
+        field.put("visible", visible);
+        field.put("fontSize", "normal");
+        field.put("align", "left");
+        field.put("bold", false);
+        field.put("divider", false);
+        field.put("blankLines", 0);
+        return field;
+    }
+
+    private JSONObject createSwitchField(String key, String titleName, boolean visible, String[] subKeys, String[] subTitles, boolean[] subVisible) {
+        JSONObject field = createTextField(key, titleName, visible);
+        JSONArray children = new JSONArray();
+        for (int i = 0; i < subKeys.length; i++) {
+            JSONObject child = new JSONObject();
+            child.put("key", subKeys[i]);
+            child.put("titleName", subTitles[i]);
+            child.put("visible", subVisible[i]);
+            children.add(child);
+        }
+        field.put("children", children);
+        return field;
+    }
+}