添加功能下载验收单

This commit is contained in:
xuqiuyun
2025-11-05 14:39:27 +08:00
parent eacb0453dd
commit 70d9db252a
11 changed files with 919 additions and 68 deletions

View File

@@ -284,6 +284,15 @@ export function downloadDeliveryPackage(id) {
});
}
// 下载验收单生成HTML格式的牛只发车验收单
export function downloadAcceptanceForm(id) {
return request({
url: `/delivery/downloadAcceptanceForm?id=${id}`,
method: 'GET',
responseType: 'blob', // 用于下载HTML文件
});
}
// 获取运送清单详情(用于编辑)
export function getDeliveryDetail(id) {
return request({

View File

@@ -52,15 +52,17 @@ axios.interceptors.response.use(
// 单独判断导出问题-start
if(toString.call(responseData) === '[object Blob]' && (responseData.type !== 'application/json')) {
download(response);
return ElMessage({
type: 'success',
message: '导出成功!',
});
ElMessage({
type: 'success',
message: '导出成功!',
});
return Promise.resolve(response.data); // 返回 Promise避免返回对象
}else if(toString.call(responseData) === '[object Blob]' && (responseData.type == 'application/json')){
setTimeout(() => {
window.location.href = '/login';
}, 1000);
return ElMessage.error(`登录过期,请重新登录!`);
ElMessage.error(`登录过期,请重新登录!`);
return Promise.reject('登录过期,请重新登录!'); // 返回 Promise.reject避免返回对象
}
// 单独判断导出问题-end
@@ -71,7 +73,8 @@ axios.interceptors.response.use(
setTimeout(() => {
window.location.href = '/login';
}, 1000);
return ElMessage.error(`登录过期,请重新登录!`);
ElMessage.error(`登录过期,请重新登录!`);
return Promise.reject('登录过期,请重新登录!'); // 返回 Promise.reject避免返回对象
}
// console.log(responseData);
// if (responseData && (responseData.code === 650)) {

View File

@@ -102,6 +102,7 @@
<el-button type="warning" link @click="editDelivery(scope.row)" v-hasPermi="['entry:edit']">编辑</el-button>
<el-button type="success" link @click="updateStatus(scope.row)" v-hasPermi="['entry:status']">修改状态</el-button>
<el-button type="info" link @click="downloadPackage(scope.row)" :loading="downLoading[scope.row.id]" v-hasPermi="['entry:download']">打包文件</el-button>
<el-button type="primary" link @click="downloadAcceptanceFormHandler(scope.row)" :loading="downLoading[scope.row.id]" v-hasPermi="['entry:export']">下载验收单</el-button>
<el-button type="danger" link @click="deleteDelivery(scope.row)" v-hasPermi="['entry:delete']">删除</el-button>
</div>
</template>
@@ -125,7 +126,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import baseSearch from '@/components/common/searchCustom/index.vue';
import createDeliveryDialog from '@/views/shipping/createDeliveryDialog.vue';
import { inspectionList, downloadZip, pageDeviceList } from '@/api/abroad.js';
import { updateDeliveryStatus, deleteDeliveryLogic, downloadDeliveryPackage, getDeliveryDetail } from '@/api/shipping.js';
import { updateDeliveryStatus, deleteDeliveryLogic, downloadDeliveryPackage, getDeliveryDetail, downloadAcceptanceForm } from '@/api/shipping.js';
const router = useRouter();
const route = useRoute();
@@ -679,10 +680,52 @@ const downloadPackage = async (row) => {
ElMessage.info('正在打包文件,请稍候...');
// 调用后端API打包文件包含图片、视频和信息
// 注意:由于响应拦截器设置了 responseType: 'blob',返回的是 Blob 对象
const res = await downloadDeliveryPackage(row.id);
// 创建下载链接
const blob = new Blob([res], { type: 'application/zip' });
// 确保 res 是 Blob 对象,如果不是则转换为 Blob
let blob;
if (res instanceof Blob) {
blob = res;
} else {
// 如果不是 Blob尝试转换为 Blob
blob = new Blob([res], { type: 'application/zip' });
}
// 检查响应是否是有效的ZIP文件通过检查ZIP文件魔数
// ZIP 文件的魔数是50 4B 03 04 (PK\x03\x04)
const arrayBuffer = await blob.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// 检查是否是ZIP文件前4个字节是 50 4B 03 04
const isZipFile = uint8Array.length >= 4 &&
uint8Array[0] === 0x50 &&
uint8Array[1] === 0x4B &&
uint8Array[2] === 0x03 &&
uint8Array[3] === 0x04;
if (!isZipFile) {
// 响应是错误信息JSON或文本尝试读取并显示错误
try {
const decoder = new TextDecoder('utf-8');
const errorText = decoder.decode(uint8Array);
let errorMsg = '打包文件失败';
try {
// 尝试解析为 JSON
const errorObj = JSON.parse(errorText);
errorMsg = errorObj.msg || errorObj.message || errorText;
} catch (e) {
// 不是 JSON直接使用文本
errorMsg = errorText || '打包文件失败,请重试';
}
ElMessage.error(errorMsg);
} catch (e) {
ElMessage.error('打包文件失败,请重试');
}
return;
}
// 创建下载链接(使用验证后的 Blob
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
@@ -695,7 +738,69 @@ const downloadPackage = async (row) => {
ElMessage.success('文件打包成功');
} catch (error) {
console.error('打包文件失败:', error);
ElMessage.error('打包文件失败,请重试');
ElMessage.error(error.message || '打包文件失败,请重试');
} finally {
downLoading[row.id] = false;
}
};
// 下载验收单
const downloadAcceptanceFormHandler = async (row) => {
try {
downLoading[row.id] = true;
ElMessage.info('正在生成验收单,请稍候...');
// 调用后端API生成验收单
const res = await downloadAcceptanceForm(row.id);
// 确保 res 是 Blob 对象
let blob;
if (res instanceof Blob) {
blob = res;
} else {
blob = new Blob([res], { type: 'text/html;charset=UTF-8' });
}
// 检查是否是HTML文件通过检查内容
const arrayBuffer = await blob.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// 尝试解码为文本检查是否是HTML
const decoder = new TextDecoder('utf-8');
const htmlText = decoder.decode(uint8Array);
// 检查是否是错误响应JSON格式
if (htmlText.trim().startsWith('{') || htmlText.trim().startsWith('[')) {
try {
const errorObj = JSON.parse(htmlText);
ElMessage.error(errorObj.msg || errorObj.message || '生成验收单失败,请重试');
} catch (e) {
ElMessage.error('生成验收单失败,请重试');
}
return;
}
// 创建下载链接
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `牛只发车验收单_${row.deliveryNumber || row.id}_${new Date().getTime()}.html`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
// 同时在新窗口中打开,方便预览
const newWindow = window.open('', '_blank');
if (newWindow) {
newWindow.document.write(htmlText);
newWindow.document.close();
}
ElMessage.success('验收单生成成功');
} catch (error) {
console.error('生成验收单失败:', error);
ElMessage.error(error.message || '生成验收单失败,请重试');
} finally {
downLoading[row.id] = false;
}

View File

@@ -224,17 +224,6 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预估重量(kg)" prop="estimatedWeight">
<el-input-number
v-model="formData.estimatedWeight"
:min="0.01"
:precision="2"
placeholder="请输入预估重量"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
@@ -671,7 +660,6 @@ const formData = reactive({
endLat: '',
endLon: '',
cattleCount: 1,
estimatedWeight: null,
quarantineCertNo: '',
remark: '',
// 装车相关字段
@@ -746,7 +734,6 @@ const rules = {
startLocation: [{ required: true, message: '请输入起点地址', trigger: 'blur' }],
endLocation: [{ required: true, message: '请输入目的地地址', trigger: 'blur' }],
cattleCount: [{ required: true, message: '请输入牛只数量', trigger: 'blur' }],
estimatedWeight: [{ required: true, message: '请输入预估重量', trigger: 'blur' }],
};
// 计算耳标设备列表(包含体重输入)
@@ -817,7 +804,6 @@ const buildSubmitData = () => {
// 其他信息
cattleCount: formData.cattleCount,
estimatedWeight: formData.estimatedWeight,
quarantineCertNo: formData.quarantineCertNo,
remark: formData.remark,
@@ -850,12 +836,24 @@ const buildSubmitData = () => {
}
// 将所有undefined值转换为空字符串确保字段被提交
// 但是对于数值类型的字段如cattleCount保留null或原值不要转换为空字符串
Object.keys(data).forEach(key => {
if (data[key] === undefined) {
data[key] = '';
// 数值类型字段保留null其他字段转换为空字符串
if (key === 'cattleCount' ||
key === 'orderId' || key === 'shipperId' || key === 'buyerId' ||
key === 'driverId' || key === 'serverId') {
data[key] = null;
} else {
data[key] = '';
}
}
});
// 确保cattleCount不为空字符串如果是空字符串则转换为null
if (data.cattleCount === '') {
data.cattleCount = null;
}
return data;
};
@@ -955,6 +953,9 @@ const fillFormWithEditData = (editData) => {
formData.entruckWeight = delivery.entruckWeight || null;
formData.landingEntruckWeight = delivery.landingEntruckWeight || null;
// 牛只数量从ratedQuantity字段映射
formData.cattleCount = delivery.ratedQuantity || 1;
// 照片
formData.quarantineTickeyUrl = delivery.quarantineTickeyUrl || '';
formData.poundListImg = delivery.poundListImg || '';
@@ -1246,7 +1247,8 @@ const handleSubmit = () => {
// 判断是编辑还是新增
if (formData.editId) {
// 编辑模式:调用更新接口
// 将cattleCount映射为ratedQuantity后端DeliveryEditDto使用ratedQuantity字段
submitData.ratedQuantity = submitData.cattleCount;
submitData.deliveryId = formData.editId; // 添加deliveryId字段后端需要
res = await shippingApi.updateDeliveryInfo(submitData);
} else {

View File

@@ -76,6 +76,17 @@
<el-option label="按肉价结算" :value="3"></el-option>
</el-select>
</el-form-item>
<el-form-item label="约定价格(元/斤)" prop="firmPrice">
<el-input-number
v-model="ruleForm.firmPrice"
:precision="2"
:min="0"
:max="9999999.99"
placeholder="请输入约定价格"
style="width: 100%"
></el-input-number>
</el-form-item>
</el-form>
<template #footer>
@@ -89,6 +100,7 @@
<script setup>
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { orderAddNew, orderUpdate } from '@/api/shipping.js';
import { memberListByType } from '@/api/userManage.js';
@@ -119,12 +131,17 @@ const ruleForm = reactive({
buyerId: [], // 买方ID数组
sellerId: [], // 卖方ID数组
settlementType: 1, // 结算方式1-上车重量2-下车重量3-按肉价结算
firmPrice: null, // 约定价格(元/斤)
});
const rules = reactive({
buyerId: [{ required: true, message: '请选择买方', trigger: 'change' }],
sellerId: [{ required: true, message: '请选择卖方', trigger: 'change' }],
settlementType: [{ required: true, message: '请选择结算方式', trigger: 'change' }],
firmPrice: [
{ required: true, message: '请输入约定价格', trigger: 'blur' },
{ type: 'number', min: 0, message: '约定价格不能小于0', trigger: 'blur' }
],
});
const handleClose = () => {
@@ -136,6 +153,7 @@ const handleClose = () => {
ruleForm.buyerId = [];
ruleForm.sellerId = [];
ruleForm.settlementType = 1;
ruleForm.firmPrice = null;
data.dialogVisible = false;
};
@@ -217,6 +235,7 @@ const onClickSave = () => {
buyerId: ruleForm.buyerId.join(','), // 将数组转为逗号分隔的字符串
sellerId: ruleForm.sellerId.join(','), // 将数组转为逗号分隔的字符串
settlementType: ruleForm.settlementType,
firmPrice: ruleForm.firmPrice, // 约定价格(元/斤)
};
data.saveLoading = true;
@@ -282,6 +301,7 @@ const onShowDialog = (orderData) => {
}
ruleForm.settlementType = orderData?.settlementType || 1;
ruleForm.firmPrice = orderData?.firmPrice != null ? orderData.firmPrice : null;
data.dialogVisible = true;
getSupplierList();

View File

@@ -19,9 +19,18 @@ import com.aiotagro.cattletrade.business.service.IXqClientService;
import com.aiotagro.common.core.context.SecurityContextHolder;
import com.aiotagro.common.core.utils.SecurityUtil;
import com.aiotagro.common.core.constant.RoleConstants;
import com.aiotagro.common.core.constant.TencentCloudConstants;
import com.aiotagro.common.core.web.domain.AjaxResult;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.GetObjectRequest;
import com.aiotagro.common.core.web.domain.PageResultResponse;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.aiotagro.cattletrade.business.mapper.MemberMapper;
import com.aiotagro.cattletrade.business.mapper.OrderMapper;
import com.aiotagro.cattletrade.business.entity.Order;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -31,8 +40,10 @@ import java.io.*;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -51,6 +62,8 @@ import java.util.zip.ZipOutputStream;
@RequestMapping("/delivery")
public class DeliveryController {
private static final Logger logger = LoggerFactory.getLogger(DeliveryController.class);
@Autowired
private IDeliveryService deliveryService;
@@ -66,6 +79,12 @@ public class DeliveryController {
@Autowired
private com.aiotagro.cattletrade.business.mapper.IotDeviceDataMapper iotDeviceDataMapper;
@Autowired
private MemberMapper memberMapper;
@Autowired
private OrderMapper orderMapper;
/**
@@ -700,10 +719,12 @@ public class DeliveryController {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOut = null;
COSClient cosClient = null;
try {
if (id == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("运单ID不能为空");
return;
}
@@ -712,6 +733,7 @@ public class DeliveryController {
Delivery delivery = deliveryService.getById(id);
if (delivery == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("运单不存在");
return;
}
@@ -723,29 +745,47 @@ public class DeliveryController {
// 1. 创建信息文本文件
addDeliveryInfoToZip(zipOut, delivery);
// 创建COS客户端如果URL是COS URL将在addFileToZip中创建
// 预先创建COS客户端避免重复创建
if (delivery.getQuarantineTickeyUrl() != null &&
delivery.getQuarantineTickeyUrl().contains(TencentCloudConstants.TENCENT_CLOUD_BUCKET + ".cos")) {
com.qcloud.cos.auth.BasicCOSCredentials cred = new com.qcloud.cos.auth.BasicCOSCredentials(
TencentCloudConstants.TENCENT_CLOUD_SECRETID,
TencentCloudConstants.TENCENT_CLOUD_SECRETKEY
);
com.qcloud.cos.region.Region region = new com.qcloud.cos.region.Region(TencentCloudConstants.TENCENT_CLOUD_REGION);
com.qcloud.cos.ClientConfig clientConfig = new com.qcloud.cos.ClientConfig(region);
clientConfig.setHttpProtocol(com.qcloud.cos.http.HttpProtocol.https);
cosClient = new COSClient(cred, clientConfig);
logger.debug("创建COS客户端用于下载文件");
}
// 2. 下载并添加所有照片
int photoCount = 0;
photoCount += addFileToZip(zipOut, delivery.getQuarantineTickeyUrl(), "照片/检疫票.jpg");
photoCount += addFileToZip(zipOut, delivery.getPoundListImg(), "照片/纸质磅单.jpg");
photoCount += addFileToZip(zipOut, delivery.getEmptyVehicleFrontPhoto(), "照片/空车过磅车头照片.jpg");
photoCount += addFileToZip(zipOut, delivery.getLoadedVehicleFrontPhoto(), "照片/车过磅车头照片.jpg");
photoCount += addFileToZip(zipOut, delivery.getLoadedVehicleWeightPhoto(), "照片/装车过磅磅单.jpg");
photoCount += addFileToZip(zipOut, delivery.getCarFrontPhoto(), "照片/车头照片.jpg");
photoCount += addFileToZip(zipOut, delivery.getCarBehindPhoto(), "照片/车照片.jpg");
photoCount += addFileToZip(zipOut, delivery.getDriverIdCardPhoto(), "照片/司机身份证照片.jpg");
photoCount += addFileToZip(zipOut, delivery.getDestinationPoundListImg(), "照片/到地磅单.jpg");
photoCount += addFileToZip(zipOut, delivery.getDestinationVehicleFrontPhoto(), "照片/到地车辆过重磅车头照片.jpg");
logger.info("开始添加照片到ZIP运单ID: {}", id);
photoCount += addFileToZip(zipOut, delivery.getQuarantineTickeyUrl(), "照片/检疫票.jpg", "检疫票", cosClient);
photoCount += addFileToZip(zipOut, delivery.getPoundListImg(), "照片/纸质磅单.jpg", "纸质磅单", cosClient);
photoCount += addFileToZip(zipOut, delivery.getEmptyVehicleFrontPhoto(), "照片/车过磅车头照片.jpg", "空车过磅车头照片", cosClient);
photoCount += addFileToZip(zipOut, delivery.getLoadedVehicleFrontPhoto(), "照片/装车过磅车头照片.jpg", "装车过磅车头照片", cosClient);
photoCount += addFileToZip(zipOut, delivery.getLoadedVehicleWeightPhoto(), "照片/装车过磅磅单.jpg", "装车过磅磅单", cosClient);
photoCount += addFileToZip(zipOut, delivery.getCarFrontPhoto(), "照片/车照片.jpg", "车头照片", cosClient);
photoCount += addFileToZip(zipOut, delivery.getCarBehindPhoto(), "照片/车尾照片.jpg", "车尾照片", cosClient);
photoCount += addFileToZip(zipOut, delivery.getDriverIdCardPhoto(), "照片/司机身份证照片.jpg", "司机身份证照片", cosClient);
photoCount += addFileToZip(zipOut, delivery.getDestinationPoundListImg(), "照片/到地磅单.jpg", "到地磅单", cosClient);
photoCount += addFileToZip(zipOut, delivery.getDestinationVehicleFrontPhoto(), "照片/到地车辆过重磅车头照片.jpg", "到地车辆过重磅车头照片", cosClient);
logger.info("照片添加完成,成功添加 {} 张照片", photoCount);
// 3. 下载并添加所有视频
int videoCount = 0;
videoCount += addFileToZip(zipOut, delivery.getEmptyWeightVideo(), "视频/空车过磅视频.mp4");
videoCount += addFileToZip(zipOut, delivery.getEntruckWeightVideo(), "视频/车过磅视频.mp4");
videoCount += addFileToZip(zipOut, delivery.getEntruckVideo(), "视频/装车视频.mp4");
videoCount += addFileToZip(zipOut, delivery.getControlSlotVideo(), "视频/消毒槽视频.mp4");
videoCount += addFileToZip(zipOut, delivery.getCattleLoadingCircleVideo(), "视频/牛只装车环视视频.mp4");
videoCount += addFileToZip(zipOut, delivery.getUnloadCattleVideo(), "视频/牛视频.mp4");
videoCount += addFileToZip(zipOut, delivery.getDestinationWeightVideo(), "视频/到地过磅视频.mp4");
logger.info("开始添加视频到ZIP运单ID: {}", id);
videoCount += addFileToZip(zipOut, delivery.getEmptyWeightVideo(), "视频/车过磅视频.mp4", "空车过磅视频", cosClient);
videoCount += addFileToZip(zipOut, delivery.getEntruckWeightVideo(), "视频/装车过磅视频.mp4", "装车过磅视频", cosClient);
videoCount += addFileToZip(zipOut, delivery.getEntruckVideo(), "视频/装车视频.mp4", "装车视频", cosClient);
videoCount += addFileToZip(zipOut, delivery.getControlSlotVideo(), "视频/消毒槽视频.mp4", "消毒槽视频", cosClient);
videoCount += addFileToZip(zipOut, delivery.getCattleLoadingCircleVideo(), "视频/牛只装车环视视频.mp4", "牛只装车环视视频", cosClient);
videoCount += addFileToZip(zipOut, delivery.getUnloadCattleVideo(), "视频/卸牛视频.mp4", "卸牛视频", cosClient);
videoCount += addFileToZip(zipOut, delivery.getDestinationWeightVideo(), "视频/到地过磅视频.mp4", "到地过磅视频", cosClient);
logger.info("视频添加完成,成功添加 {} 个视频", videoCount);
zipOut.finish();
@@ -767,12 +807,16 @@ public class DeliveryController {
} catch (Exception e) {
e.printStackTrace();
logger.error("打包文件失败运单ID: {}", id, e);
try {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("打包文件失败:" + e.getMessage());
// 确保响应头已设置,避免被错误处理为登录过期
if (!response.isCommitted()) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("打包文件失败:" + e.getMessage());
}
} catch (IOException ioException) {
ioException.printStackTrace();
logger.error("写入错误响应失败", ioException);
}
} finally {
try {
@@ -780,8 +824,12 @@ public class DeliveryController {
zipOut.close();
}
byteArrayOutputStream.close();
// 关闭COS客户端
if (cosClient != null) {
cosClient.shutdown();
}
} catch (IOException e) {
e.printStackTrace();
logger.warn("关闭资源失败", e);
}
}
}
@@ -822,45 +870,663 @@ public class DeliveryController {
/**
* 从URL下载文件并添加到ZIP
* 优先使用腾讯云COS SDK下载如果URL不是COS URL则使用HttpURLConnection
* @param zipOut ZIP输出流
* @param fileUrl 文件URL
* @param fileName ZIP中的文件名
* @param fileDescription 文件描述(用于日志)
* @param cosClient COS客户端如果已创建可复用如果为null会在需要时创建
* @return 成功添加返回1失败或URL为空返回0
*/
private int addFileToZip(ZipOutputStream zipOut, String fileUrl, String fileName) {
private int addFileToZip(ZipOutputStream zipOut, String fileUrl, String fileName, String fileDescription, COSClient cosClient) {
if (fileUrl == null || fileUrl.trim().isEmpty()) {
logger.debug("跳过添加文件: {} (URL为空)", fileDescription);
return 0;
}
InputStream inputStream = null;
try {
logger.debug("开始下载文件: {}URL: {}", fileDescription, fileUrl);
URL url = new URL(fileUrl);
inputStream = url.openStream();
// 检查是否是腾讯云COS的URL
if (fileUrl.contains(TencentCloudConstants.TENCENT_CLOUD_BUCKET + ".cos")) {
// 使用COS SDK下载文件
try {
// 从URL中提取COS对象键key
// URL格式: https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/11/04/文件名.jpg
String cosKey = extractCosKeyFromUrl(fileUrl);
if (cosKey == null) {
logger.warn("无法从URL中提取COS键: {}URL: {}", fileDescription, fileUrl);
return 0;
}
// 使用COS客户端下载文件
// 如果传入的cosClient为null则创建新的客户端
COSClient clientToUse = cosClient;
if (clientToUse == null) {
com.qcloud.cos.auth.BasicCOSCredentials cred = new com.qcloud.cos.auth.BasicCOSCredentials(
TencentCloudConstants.TENCENT_CLOUD_SECRETID,
TencentCloudConstants.TENCENT_CLOUD_SECRETKEY
);
com.qcloud.cos.region.Region region = new com.qcloud.cos.region.Region(TencentCloudConstants.TENCENT_CLOUD_REGION);
com.qcloud.cos.ClientConfig clientConfig = new com.qcloud.cos.ClientConfig(region);
clientConfig.setHttpProtocol(com.qcloud.cos.http.HttpProtocol.https);
clientToUse = new COSClient(cred, clientConfig);
}
GetObjectRequest getObjectRequest = new GetObjectRequest(
TencentCloudConstants.TENCENT_CLOUD_BUCKET,
cosKey
);
com.qcloud.cos.model.COSObject cosObject = clientToUse.getObject(getObjectRequest);
inputStream = cosObject.getObjectContent();
logger.debug("使用COS SDK下载文件: {}COS键: {}", fileDescription, cosKey);
} catch (Exception e) {
logger.warn("使用COS SDK下载文件失败: {}URL: {},错误: {}尝试使用HttpURLConnection",
fileDescription, fileUrl, e.getMessage());
// 如果COS SDK失败回退到HttpURLConnection
inputStream = downloadViaHttp(fileUrl);
}
} else {
// 非COS URL使用HttpURLConnection
inputStream = downloadViaHttp(fileUrl);
}
if (inputStream == null) {
logger.warn("无法获取文件输入流: {}URL: {}", fileDescription, fileUrl);
return 0;
}
ZipEntry entry = new ZipEntry(fileName);
zipOut.putNextEntry(entry);
byte[] buffer = new byte[8192];
int bytesRead;
int totalBytes = 0;
long totalBytes = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
zipOut.write(buffer, 0, bytesRead);
totalBytes += bytesRead;
}
zipOut.closeEntry();
logger.debug("成功添加文件: {},大小: {} 字节", fileDescription, totalBytes);
return 1;
} catch (Exception e) {
logger.error("添加文件到ZIP时发生错误: {}URL: {}", fileDescription, fileUrl, e);
return 0;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
logger.warn("关闭输入流失败: {}", e.getMessage());
}
}
}
}
/**
* 从COS URL中提取对象键key
* @param fileUrl COS文件URL
* @return COS对象键如果无法提取则返回null
*/
private String extractCosKeyFromUrl(String fileUrl) {
try {
// URL格式: https://smart-1251449951.cos.ap-guangzhou.myqcloud.com/iotPlateform/2025/11/04/文件名.jpg
URL url = new URL(fileUrl);
String path = url.getPath();
if (path != null && path.startsWith("/")) {
return path.substring(1); // 移除开头的 "/"
}
return path;
} catch (Exception e) {
logger.warn("提取COS键失败URL: {},错误: {}", fileUrl, e.getMessage());
return null;
}
}
/**
* 使用HttpURLConnection下载文件
* @param fileUrl 文件URL
* @return 输入流如果失败返回null
*/
private InputStream downloadViaHttp(String fileUrl) {
try {
// 对URL进行编码处理特别是中文字符
URL url = new URL(fileUrl);
java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
connection.setConnectTimeout(10000); // 10秒连接超时
connection.setReadTimeout(30000); // 30秒读取超时
connection.setRequestMethod("GET");
connection.setRequestProperty("User-Agent", "Mozilla/5.0");
int responseCode = connection.getResponseCode();
if (responseCode != java.net.HttpURLConnection.HTTP_OK) {
logger.warn("HTTP下载失败响应码: {}URL: {}", responseCode, fileUrl);
return null;
}
return connection.getInputStream();
} catch (Exception e) {
logger.warn("HttpURLConnection下载失败URL: {},错误: {}", fileUrl, e.getMessage());
return null;
}
}
/**
* 下载验收单生成HTML格式的牛只发车验收单
*
* @param id 运单ID
* @param response HTTP响应
*/
@SaCheckPermission("delivery:export")
@GetMapping("/downloadAcceptanceForm")
public void downloadAcceptanceForm(@RequestParam Integer id, HttpServletResponse response) {
logger.info("开始生成验收单运单ID: {}", id);
try {
if (id == null) {
logger.warn("运单ID不能为空");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("运单ID不能为空");
return;
}
// 查询运单详情
Delivery delivery = deliveryService.getById(id);
if (delivery == null) {
logger.warn("运单不存在ID: {}", id);
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("运单不存在");
return;
}
logger.info("查询到运单信息,运单号: {}, 车牌号: {}", delivery.getDeliveryNumber(), delivery.getLicensePlate());
// 查询供应商名称如果supplierName为空
if (delivery.getSupplierName() == null || delivery.getSupplierName().isEmpty()) {
if (delivery.getSupplierId() != null && !delivery.getSupplierId().isEmpty()) {
try {
String[] supplierIds = delivery.getSupplierId().split(",");
List<String> supplierNames = new ArrayList<>();
for (String supplierId : supplierIds) {
if (StringUtils.isNotEmpty(supplierId.trim())) {
try {
Integer sid = Integer.parseInt(supplierId.trim());
Map<String, Object> supplierInfo = memberMapper.selectMemberUserById(sid);
if (supplierInfo != null) {
String username = (String) supplierInfo.get("username");
if (StringUtils.isNotEmpty(username)) {
supplierNames.add(username);
}
}
} catch (NumberFormatException e) {
logger.warn("供应商ID格式错误: {}", supplierId);
}
}
}
if (!supplierNames.isEmpty()) {
delivery.setSupplierName(String.join(",", supplierNames));
}
} catch (Exception e) {
logger.error("查询供应商信息失败运单ID: {}", id, e);
}
}
}
// 查询采购商名称如果buyerName为空
if (delivery.getBuyerName() == null || delivery.getBuyerName().isEmpty()) {
if (delivery.getBuyerId() != null) {
try {
Map<String, Object> buyerInfo = memberMapper.selectMemberUserById(delivery.getBuyerId());
if (buyerInfo != null) {
String username = (String) buyerInfo.get("username");
String mobile = (String) buyerInfo.get("mobile");
if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(mobile)) {
delivery.setBuyerName(username + "/" + mobile);
} else if (StringUtils.isNotEmpty(username)) {
delivery.setBuyerName(username);
} else if (StringUtils.isNotEmpty(mobile)) {
delivery.setBuyerName(mobile);
}
}
} catch (Exception e) {
logger.error("查询采购商信息失败运单ID: {}", id, e);
}
}
}
// 准备数据 - 供货单位(卖方字段,只取姓名,不要手机号)
String supplierName = delivery.getSupplierName();
if (supplierName != null && !supplierName.isEmpty()) {
// 如果包含"/",取"/"前面的部分(姓名部分)
if (supplierName.contains("/")) {
supplierName = supplierName.split("/")[0].trim();
}
// 如果包含逗号分隔(多个供应商),取第一个并处理"/"
if (supplierName.contains(",")) {
String[] parts = supplierName.split(",");
supplierName = parts[0].trim();
if (supplierName.contains("/")) {
supplierName = supplierName.split("/")[0].trim();
}
}
} else {
// 如果supplierName为空使用supplierMobile作为备选但通常不应该这样做因为这是姓名字段
supplierName = "";
}
// 准备数据 - 收货单位(买方字段,只取姓名,不要手机号)
String buyerName = delivery.getBuyerName();
if (buyerName != null && !buyerName.isEmpty()) {
// 如果包含"/",取"/"前面的部分(姓名部分)
if (buyerName.contains("/")) {
buyerName = buyerName.split("/")[0].trim();
}
// 如果包含逗号分隔(多个采购商),取第一个并处理"/"
if (buyerName.contains(",")) {
String[] parts = buyerName.split(",");
buyerName = parts[0].trim();
if (buyerName.contains("/")) {
buyerName = buyerName.split("/")[0].trim();
}
}
} else {
// 如果buyerName为空使用buyerMobile作为备选但通常不应该这样做因为这是姓名字段
buyerName = "";
}
logger.info("提取的供货单位: {}, 收货单位: {}", supplierName, buyerName);
String startLocation = delivery.getStartLocation() != null ? delivery.getStartLocation() : "";
String endLocation = delivery.getEndLocation() != null ? delivery.getEndLocation() : "";
String licensePlate = delivery.getLicensePlate() != null ? delivery.getLicensePlate() : "";
// 格式化发车时间
String departureTime = "";
if (delivery.getEstimatedDepartureTime() != null) {
try {
// Date 转 LocalDateTime
java.time.LocalDateTime localDateTime = delivery.getEstimatedDepartureTime().toInstant()
.atZone(java.time.ZoneId.systemDefault())
.toLocalDateTime();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd");
departureTime = localDateTime.format(formatter);
} catch (Exception e) {
logger.warn("格式化发车时间失败使用默认格式运单ID: {}", id, e);
// 如果格式化失败,使用默认格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd");
departureTime = sdf.format(delivery.getEstimatedDepartureTime());
}
}
// 处理司机姓名和联系方式
String driverName = delivery.getDriverName() != null ? delivery.getDriverName() : "";
String driverMobile = delivery.getDriverMobile() != null ? delivery.getDriverMobile() : "";
String driverInfo = "";
if (!driverName.isEmpty() && driverName.contains("/")) {
// 如果司机姓名包含"/",取前面部分作为姓名
String[] parts = driverName.split("/");
driverInfo = parts[0];
if (parts.length > 1) {
driverInfo += " " + parts[1];
} else if (!driverMobile.isEmpty()) {
driverInfo += " " + driverMobile;
}
} else {
driverInfo = driverName;
if (!driverMobile.isEmpty()) {
driverInfo += " " + driverMobile;
}
}
// 获取检疫证号从检疫票URL中提取如果有的话
String quarantineCertNo = "";
if (delivery.getQuarantineTickeyUrl() != null && !delivery.getQuarantineTickeyUrl().isEmpty()) {
// 可以根据实际情况提取检疫证号,这里暂时留空
quarantineCertNo = "";
}
// 获取订单编号优先使用deliveryNumber如果没有则使用orderId
String orderNumber = delivery.getDeliveryNumber() != null ? delivery.getDeliveryNumber() : "";
if (orderNumber.isEmpty() && delivery.getOrderId() != null) {
orderNumber = "ORDER-" + delivery.getOrderId();
}
// 查询订单信息,获取结算方式描述、单价等信息
String weightColumnTitle = "下车总重量(斤)"; // 默认值
Integer settlementType = null; // 结算方式1-上车重量2-下车重量3-按肉价结算
java.math.BigDecimal unitPrice = null; // 单价(元/斤)
if (delivery.getOrderId() != null) {
try {
Order order = orderMapper.selectById(delivery.getOrderId());
if (order != null) {
// 获取结算方式
if (order.getSettlementType() != null) {
settlementType = order.getSettlementType();
if (settlementType == 1) {
// 上车重量 -> 上车总重量(斤)
weightColumnTitle = "上车总重量(斤)";
logger.info("订单结算方式为上车重量,使用重量字段标题: {}", weightColumnTitle);
} else if (settlementType == 2) {
// 下车重量 -> 下车总重量(斤)
weightColumnTitle = "下车总重量(斤)";
logger.info("订单结算方式为下车重量,使用重量字段标题: {}", weightColumnTitle);
} else if (settlementType == 3) {
// 按肉价结算 -> 保持默认"下车总重量(斤)"
weightColumnTitle = "下车总重量(斤)";
logger.info("订单结算方式为按肉价结算,使用默认重量字段标题: {}", weightColumnTitle);
}
}
// 获取单价(元/斤)
if (order.getFirmPrice() != null) {
unitPrice = order.getFirmPrice();
logger.info("获取订单单价: firmPrice={}", unitPrice);
} else {
logger.warn("订单单价为空订单ID: {}", delivery.getOrderId());
}
} else {
logger.warn("订单不存在订单ID: {}, 使用默认值", delivery.getOrderId());
}
} catch (Exception e) {
logger.error("查询订单信息失败订单ID: {}", delivery.getOrderId(), e);
}
} else {
logger.warn("运单没有关联订单,使用默认值");
}
// 计算总重量(斤)和总金额(元)
Integer ratedQuantity = delivery.getRatedQuantity(); // 下车总数量(头)
Double totalWeightJin = null; // 总重量(斤)
Double totalAmount = null; // 总金额(元)
// 解析重量字段String类型如"1000.00"
Double emptyWeightKg = null;
Double entruckWeightKg = null;
Double landingEntruckWeightKg = null;
try {
if (delivery.getEmptyWeight() != null && !delivery.getEmptyWeight().trim().isEmpty()) {
emptyWeightKg = Double.parseDouble(delivery.getEmptyWeight().trim());
}
if (delivery.getEntruckWeight() != null && !delivery.getEntruckWeight().trim().isEmpty()) {
entruckWeightKg = Double.parseDouble(delivery.getEntruckWeight().trim());
}
if (delivery.getLandingEntruckWeight() != null && !delivery.getLandingEntruckWeight().trim().isEmpty()) {
landingEntruckWeightKg = Double.parseDouble(delivery.getLandingEntruckWeight().trim());
}
// 根据结算方式计算总重量(斤)
// 注意:重量字段存储的是公斤(kg),需要转换为斤(斤 = kg * 2)
if (settlementType != null && emptyWeightKg != null) {
if (settlementType == 1) {
// 上车重量:计算 (entruckWeight - emptyWeight) * 2
if (entruckWeightKg != null) {
totalWeightJin = (entruckWeightKg - emptyWeightKg) * 2;
logger.info("计算上车总重量: entruckWeight={}kg, emptyWeight={}kg, 总重量={}斤",
entruckWeightKg, emptyWeightKg, totalWeightJin);
}
} else if (settlementType == 2) {
// 下车重量:计算 (landingEntruckWeight - emptyWeight) * 2
if (landingEntruckWeightKg != null) {
totalWeightJin = (landingEntruckWeightKg - emptyWeightKg) * 2;
logger.info("计算下车总重量: landingEntruckWeight={}kg, emptyWeight={}kg, 总重量={}斤",
landingEntruckWeightKg, emptyWeightKg, totalWeightJin);
}
}
}
// 计算总金额(元) = 总重量(斤) * 单价(元/斤)
if (totalWeightJin != null && unitPrice != null) {
totalAmount = totalWeightJin * unitPrice.doubleValue();
// 四舍五入保留2位小数
totalAmount = java.math.BigDecimal.valueOf(totalAmount)
.setScale(2, java.math.RoundingMode.HALF_UP)
.doubleValue();
logger.info("计算总金额: 总重量={}斤, 单价={}元/斤, 总金额={}元",
totalWeightJin, unitPrice, totalAmount);
}
} catch (NumberFormatException e) {
logger.error("解析重量字段失败: emptyWeight={}, entruckWeight={}, landingEntruckWeight={}",
delivery.getEmptyWeight(), delivery.getEntruckWeight(),
delivery.getLandingEntruckWeight(), e);
}
// 格式化数值为字符串保留2位小数
String ratedQuantityStr = ratedQuantity != null ? String.valueOf(ratedQuantity) : "";
String totalWeightStr = totalWeightJin != null ?
String.format("%.2f", totalWeightJin) : "";
String unitPriceStr = unitPrice != null ?
unitPrice.setScale(2, java.math.RoundingMode.HALF_UP).toString() : "";
String totalAmountStr = totalAmount != null ?
String.format("%.2f", totalAmount) : "";
// 生成HTML内容
String htmlContent = generateAcceptanceFormHtml(
orderNumber, supplierName, buyerName, startLocation,
departureTime, endLocation, quarantineCertNo,
driverInfo, licensePlate, weightColumnTitle,
ratedQuantityStr, totalWeightStr, unitPriceStr, totalAmountStr
);
// 设置响应头
response.setContentType("text/html;charset=UTF-8");
response.setHeader("Content-Disposition",
"inline; filename=\"" + URLEncoder.encode("牛只发车验收单_" + orderNumber + ".html", "UTF-8") + "\"");
// 写入响应
response.getWriter().write(htmlContent);
response.getWriter().flush();
logger.info("验收单生成成功运单ID: {}, 运单号: {}", id, orderNumber);
} catch (Exception e) {
logger.error("生成验收单失败运单ID: {}", id, e);
try {
if (!response.isCommitted()) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("生成验收单失败:" + e.getMessage());
}
} catch (IOException ioException) {
logger.error("写入错误响应失败", ioException);
}
}
}
/**
* 生成验收单HTML内容严格按照图片格式要求
* @param weightColumnTitle 重量字段标题(根据结算方式动态设置:上车总重量(斤) 或 下车总重量(斤)
* @param ratedQuantity 下车总数量(头)
* @param totalWeight 总重量(斤)
* @param unitPrice 单价(元/斤)
* @param totalAmount 总金额(元)
*/
private String generateAcceptanceFormHtml(String orderNumber, String supplierName,
String buyerName, String startLocation,
String departureTime, String endLocation,
String quarantineCertNo, String driverInfo,
String licensePlate, String weightColumnTitle,
String ratedQuantity, String totalWeight,
String unitPrice, String totalAmount) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n");
html.append("<html>\n");
html.append("<head>\n");
html.append(" <meta charset=\"UTF-8\">\n");
html.append(" <title>牛只发车验收单</title>\n");
html.append(" <style>\n");
html.append(" @media print {\n");
html.append(" body { margin: 0; }\n");
html.append(" .no-print { display: none; }\n");
html.append(" }\n");
html.append(" body { \n");
html.append(" font-family: \"Microsoft YaHei\", \"SimSun\", Arial, sans-serif; \n");
html.append(" margin: 20px; \n");
html.append(" line-height: 1.4;\n");
html.append(" }\n");
html.append(" .header { \n");
html.append(" text-align: center; \n");
html.append(" font-size: 20px; \n");
html.append(" font-weight: bold; \n");
html.append(" margin-bottom: 30px;\n");
html.append(" border-bottom: 2px solid #000;\n");
html.append(" padding-bottom: 10px;\n");
html.append(" }\n");
html.append(" .order-number { \n");
html.append(" text-align: right; \n");
html.append(" margin-bottom: 10px; \n");
html.append(" font-size: 14px;\n");
html.append(" }\n");
html.append(" table { \n");
html.append(" width: 100%; \n");
html.append(" border-collapse: collapse; \n");
html.append(" margin-bottom: 20px; \n");
html.append(" }\n");
html.append(" table td, table th { \n");
html.append(" border: 1px solid #000; \n");
html.append(" padding: 8px; \n");
html.append(" text-align: left; \n");
html.append(" }\n");
html.append(" table th { \n");
html.append(" background-color: #f0f0f0; \n");
html.append(" font-weight: bold; \n");
html.append(" }\n");
html.append(" .print-button { \n");
html.append(" padding: 10px 20px; \n");
html.append(" background-color: #409EFF; \n");
html.append(" color: white; \n");
html.append(" border: none; \n");
html.append(" border-radius: 4px; \n");
html.append(" cursor: pointer; \n");
html.append(" font-size: 14px; \n");
html.append(" }\n");
html.append(" .print-button:hover { \n");
html.append(" background-color: #66b1ff; \n");
html.append(" }\n");
html.append(" </style>\n");
html.append("</head>\n");
html.append("<body>\n");
// 附件2左上角
html.append(" <div style=\"text-align: left; margin-bottom: 10px;\">附件2</div>\n");
// 标题(居中,带下划线)
html.append(" <div class=\"header\">牛只发车验收单</div>\n");
// 订单编号(右上角)
html.append(" <div class=\"order-number\">订单编号: ").append(orderNumber).append("</div>\n");
// 基本信息表格4行2列
html.append(" <table>\n");
html.append(" <tr>\n");
html.append(" <td style=\"width: 15%;\">供货单位</td>\n");
html.append(" <td style=\"width: 35%;\">").append(supplierName).append("</td>\n");
html.append(" <td style=\"width: 15%;\">收货单位</td>\n");
html.append(" <td style=\"width: 35%;\">").append(buyerName).append("</td>\n");
html.append(" </tr>\n");
html.append(" <tr>\n");
html.append(" <td>发车地点</td>\n");
html.append(" <td>").append(startLocation).append("</td>\n");
html.append(" <td>发车时间</td>\n");
html.append(" <td>").append(departureTime).append("</td>\n");
html.append(" </tr>\n");
html.append(" <tr>\n");
html.append(" <td>到达地点</td>\n");
html.append(" <td>").append(endLocation).append("</td>\n");
html.append(" <td>动物检疫合格证明编号</td>\n");
html.append(" <td>").append(quarantineCertNo).append("</td>\n");
html.append(" </tr>\n");
html.append(" <tr>\n");
html.append(" <td>司机姓名及联系方式</td>\n");
html.append(" <td>").append(driverInfo).append("</td>\n");
html.append(" <td>装车车牌号</td>\n");
html.append(" <td>").append(licensePlate).append("</td>\n");
html.append(" </tr>\n");
html.append(" </table>\n");
// 牛只明细表格(完整保留,但数据为空)
html.append(" <table>\n");
html.append(" <tr>\n");
html.append(" <th style=\"width: 5%;\">序号</th>\n");
html.append(" <th style=\"width: 15%;\">活牛品种</th>\n");
html.append(" <th style=\"width: 15%;\">单只体重范围</th>\n");
html.append(" <th style=\"width: 12%;\">下车总数量(头)</th>\n");
html.append(" <th style=\"width: 12%;\">").append(weightColumnTitle).append("</th>\n");
html.append(" <th style=\"width: 10%;\">单价(元/斤)</th>\n");
html.append(" <th style=\"width: 15%;\">总金额(元)</th>\n");
html.append(" <th style=\"width: 16%;\">备注</th>\n");
html.append(" </tr>\n");
html.append(" <tr>\n");
html.append(" <td></td>\n");
html.append(" <td></td>\n");
html.append(" <td></td>\n");
html.append(" <td>").append(ratedQuantity).append("</td>\n");
html.append(" <td>").append(totalWeight).append("</td>\n");
html.append(" <td>").append(unitPrice).append("</td>\n");
html.append(" <td>").append(totalAmount).append("</td>\n");
html.append(" <td></td>\n");
html.append(" </tr>\n");
html.append(" </table>\n");
// 支付信息表格(完整保留,但数据为空)
html.append(" <table>\n");
html.append(" <tr>\n");
html.append(" <td style=\"width: 15%;\">已支付货款时间</td>\n");
html.append(" <td style=\"width: 35%;\"></td>\n");
html.append(" <td style=\"width: 15%;\">已支付货款金额</td>\n");
html.append(" <td style=\"width: 35%;\"></td>\n");
html.append(" </tr>\n");
html.append(" <tr>\n");
html.append(" <td>应支付尾款时间</td>\n");
html.append(" <td></td>\n");
html.append(" <td>应支付尾款金额</td>\n");
html.append(" <td></td>\n");
html.append(" </tr>\n");
html.append(" </table>\n");
// 验收结论和签字盖章区域(完整保留,但数据为空)
html.append(" <table>\n");
html.append(" <tr>\n");
html.append(" <td style=\"width: 15%;\">验收结论</td>\n");
html.append(" <td style=\"width: 35%;\"></td>\n");
html.append(" <td style=\"width: 15%;\">验收时间</td>\n");
html.append(" <td style=\"width: 35%;\"></td>\n");
html.append(" </tr>\n");
html.append(" <tr>\n");
html.append(" <td>供货单位指定验收人签字及联系方式</td>\n");
html.append(" <td></td>\n");
html.append(" <td>收货单位指定验收人签字及联系方式</td>\n");
html.append(" <td></td>\n");
html.append(" </tr>\n");
html.append(" <tr>\n");
html.append(" <td>供货单位盖章</td>\n");
html.append(" <td></td>\n");
html.append(" <td>收货单位盖章</td>\n");
html.append(" <td></td>\n");
html.append(" </tr>\n");
html.append(" </table>\n");
// 打印按钮(不打印时显示)
html.append(" <div class=\"no-print\">\n");
html.append(" <button class=\"print-button\" onclick=\"window.print()\">打印/保存为PDF</button>\n");
html.append(" <p style=\"color: #666; font-size: 12px;\">\n");
html.append(" 提示:点击\"打印/保存为PDF\"按钮可以将此文档打印或保存为PDF格式。\n");
html.append(" 在打印对话框中,您也可以选择\"另存为PDF\"来保存文档。\n");
html.append(" </p>\n");
html.append(" </div>\n");
html.append("</body>\n");
html.append("</html>\n");
return html.toString();
}
}

View File

@@ -107,13 +107,6 @@ public class DeliveryCreateDto {
@Min(value = 1, message = "牛只数量至少为1")
private Integer cattleCount;
/**
* 预估重量(公斤)
*/
@NotNull(message = "预估重量不能为空")
@DecimalMin(value = "0.01", message = "预估重量必须大于0")
private Double estimatedWeight;
/**
* 检疫证号
*/

View File

@@ -47,6 +47,12 @@ public class Order implements Serializable {
@TableField("settlement_type")
private Integer settlementType;
/**
* 约定价格(元/斤)
*/
@TableField("firm_price")
private java.math.BigDecimal firmPrice;
/**
* 逻辑删除标记(0-正常,1-已删除)
*/

View File

@@ -468,8 +468,8 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
@Override
public AjaxResult createDelivery(DeliveryCreateDto dto) {
// 入参日志
logger.info("创建装车订单: shipperId={}, buyerId={}, plateNumber={}, driverName={}",
dto.getShipperId(), dto.getBuyerId(), dto.getPlateNumber(), dto.getDriverName());
logger.info("创建装车订单: shipperId={}, buyerId={}, plateNumber={}, driverName={}, cattleCount={}",
dto.getShipperId(), dto.getBuyerId(), dto.getPlateNumber(), dto.getDriverName(), dto.getCattleCount());
// 校验时间逻辑
if (dto.getEstimatedArrivalTime().before(dto.getEstimatedDepartureTime())) {
return AjaxResult.error("预计到达时间必须晚于出发时间");
@@ -546,6 +546,14 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
delivery.setEstimatedDepartureTime(dto.getEstimatedDepartureTime());
// 预计到达时间
delivery.setEstimatedDeliveryTime(dto.getEstimatedArrivalTime());
// 装车数量(牛只数量)
if (dto.getCattleCount() != null && dto.getCattleCount() > 0) {
delivery.setRatedQuantity(dto.getCattleCount());
logger.info("设置装车数量: ratedQuantity={}", dto.getCattleCount());
} else {
logger.warn("装车数量为空或无效: cattleCount={}, 跳过设置", dto.getCattleCount());
delivery.setRatedQuantity(null);
}
// 过磅重量将空字符串转换为null避免数据库DECIMAL字段类型错误
delivery.setEmptyWeight(StringUtils.isNotEmpty(dto.getEmptyWeight()) ? dto.getEmptyWeight() : null);
delivery.setEntruckWeight(StringUtils.isNotEmpty(dto.getEntruckWeight()) ? dto.getEntruckWeight() : null);
@@ -592,8 +600,9 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
// 保存运送清单
boolean saved = this.save(delivery);
logger.info("保存装车订单: deliveryNumber={}, licensePlate={}, driverName={}, status={}",
delivery.getDeliveryNumber(), delivery.getLicensePlate(), delivery.getDriverName(), delivery.getStatus());
logger.info("保存装车订单: deliveryNumber={}, licensePlate={}, driverName={}, status={}, ratedQuantity={}",
delivery.getDeliveryNumber(), delivery.getLicensePlate(), delivery.getDriverName(),
delivery.getStatus(), delivery.getRatedQuantity());
if (!saved) {
return AjaxResult.error("创建失败");
}
@@ -679,6 +688,9 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
// 获取当前登录用户
Integer userId = SecurityUtil.getCurrentUserId();
// 入参日志
logger.info("更新运送清单: deliveryId={}, ratedQuantity={}", dto.getDeliveryId(), dto.getRatedQuantity());
// 查询运送清单是否存在
Delivery delivery = this.getById(dto.getDeliveryId());
if (delivery == null) {
@@ -694,8 +706,11 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
if (dto.getDeliveryTitle() != null) {
delivery.setDeliveryTitle(dto.getDeliveryTitle());
}
if (dto.getRatedQuantity() != null) {
if (dto.getRatedQuantity() != null && dto.getRatedQuantity() > 0) {
delivery.setRatedQuantity(dto.getRatedQuantity());
logger.info("更新装车数量: ratedQuantity={}", dto.getRatedQuantity());
} else if (dto.getRatedQuantity() != null && dto.getRatedQuantity() <= 0) {
logger.warn("装车数量无效: ratedQuantity={}, 跳过更新", dto.getRatedQuantity());
}
if (dto.getStartLocation() != null) {
delivery.setStartLocation(dto.getStartLocation());
@@ -775,6 +790,7 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
// 保存更新
boolean updated = this.updateById(delivery);
logger.info("更新运送清单成功: deliveryId={}, ratedQuantity={}", dto.getDeliveryId(), delivery.getRatedQuantity());
if (updated) {
return AjaxResult.success("编辑成功");
} else {

View File

@@ -119,7 +119,8 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
@Override
@Transactional
public AjaxResult addOrder(Order order) {
logger.info("开始新增订单,买方:{},卖方:{},结算方式:{}", order.getBuyerId(), order.getSellerId(), order.getSettlementType());
logger.info("开始新增订单,买方:{},卖方:{},结算方式:{},约定价格:{}",
order.getBuyerId(), order.getSellerId(), order.getSettlementType(), order.getFirmPrice());
try {
// 验证结算方式
@@ -128,6 +129,17 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
return AjaxResult.error("结算方式无效");
}
// 验证约定价格firm_price 字段是 NOT NULL
if (order.getFirmPrice() == null) {
logger.error("约定价格不能为空");
return AjaxResult.error("约定价格不能为空");
}
// 确保约定价格不为负数
if (order.getFirmPrice().compareTo(java.math.BigDecimal.ZERO) < 0) {
logger.error("约定价格不能小于0{}", order.getFirmPrice());
return AjaxResult.error("约定价格不能小于0");
}
// 设置创建人和创建时间
Integer userId = SecurityUtil.getCurrentUserId();
order.setCreatedBy(userId);
@@ -160,7 +172,7 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
return AjaxResult.error("订单ID不能为空");
}
logger.info("开始更新订单订单ID{}", order.getId());
logger.info("开始更新订单订单ID{},约定价格:{}", order.getId(), order.getFirmPrice());
try {
// 验证订单是否存在
@@ -176,6 +188,18 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
return AjaxResult.error("结算方式无效");
}
// 验证约定价格(如果提供了)
if (order.getFirmPrice() != null) {
// 确保约定价格不为负数
if (order.getFirmPrice().compareTo(java.math.BigDecimal.ZERO) < 0) {
logger.error("约定价格不能小于0{}", order.getFirmPrice());
return AjaxResult.error("约定价格不能小于0");
}
} else {
// 如果更新时没有提供 firm_price保留原有值因为字段是 NOT NULL
order.setFirmPrice(existingOrder.getFirmPrice());
}
// 设置更新人和更新时间
try {
Integer userId = SecurityUtil.getCurrentUserId();

View File

@@ -81,5 +81,12 @@ public class TencentCloudUtil {
return targetUrl;
}
/**
* 获取COS客户端
* @return COS客户端实例
*/
public static COSClient getCosClient() {
return cosClient;
}
}