add:新增知识库功能

This commit is contained in:
zhangzq
2026-01-31 15:51:23 +08:00
parent 42dd6d29f0
commit 36a20f3ddc
23 changed files with 650 additions and 347 deletions

View File

@@ -0,0 +1,38 @@
# Word模板标签文本直接复制使用
## 循环开始标签
{{#rows}}
## 循环结束标签
{{/rows}}
## 数据字段标签
{{seq}}
{{materialName}}
{{materialSpec}}
{{qty}}
{{unitName}}
{{salePrice}}
{{amount}}
{{remark}}
## 其他字段标签
{{totalPrice}}
{{clientName}}
{{contractCode}}
{{effectiveDate}}
---
## 使用说明
1. 从本文件复制标签文本
2. 粘贴到Word模板的对应位置
3. 确保使用"无格式文本"粘贴
## 注意事项
- 标签必须完全匹配,包括大小写
- 不要有多余的空格
- 使用英文字符,不要用中文字符
- {{#rows}} 中的 # 是英文井号

View File

@@ -0,0 +1,243 @@
# Word合同模板配置说明
## 📋 问题原因
Word模板中的动态列表数据渲染失败主要原因是
1. 没有配置 `LoopRowTableRenderPolicy` 循环行渲染策略
2. 数据格式不匹配直接传JSONArray而不是List<Map>
3. 模板语法使用不正确
## ✅ 解决方案
### 1. Word模板正确语法
在Word模板的表格中需要使用以下格式
```
┌─────────┬──────────┬──────────┬──────┬──────┬──────────┬──────────┬──────┐
│ 序号 │ 产品名称 │ 型号 │ 数量 │ 单位 │ 单价(元) │ 总价(元) │ 备注 │
├─────────┼──────────┼──────────┼──────┼──────┼──────────┼──────────┼──────┤
│{{#rows}}│ │ │ │ │ │ │ │
├─────────┼──────────┼──────────┼──────┼──────┼──────────┼──────────┼──────┤
│{{seq}} │{{materialName}}│{{materialSpec}}│{{qty}}│{{unitName}}│{{salePrice}}│{{amount}}│{{remark}}│
├─────────┼──────────┼──────────┼──────┼──────┼──────────┼──────────┼──────┤
│{{/rows}}│ │ │ │ │ │ │ │
└─────────┴──────────┴──────────┴──────┴──────┴──────────┴──────────┴──────┘
```
**重要说明**
- `{{#rows}}` 必须单独占一行(表格的一行)
- 数据行包含所有字段:`{{seq}}`, `{{materialName}}`
- `{{/rows}}` 必须单独占一行(表格的一行)
- 这三行构成一个完整的循环结构
### 2. 模板文件位置
将模板文件放在以下位置之一:
**方式1放在resources目录推荐**
```
src/main/resources/templates/contract_template.docx
```
**方式2使用绝对路径**
```java
/Users/mima0000/Desktop/合同.docx
```
### 3. 后端代码关键点
#### 3.1 配置循环行策略
```java
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder()
.bind("rows", policy) // 绑定rows标签
.build();
```
#### 3.2 数据格式转换
```java
// 错误方式 ❌
dataMap.put("rows", JSONArray.parseArray(materialJson));
// 正确方式 ✅
List<Map<String, Object>> rowsList = new ArrayList<>();
for (int i = 0; i < materialArray.size(); i++) {
JSONObject item = materialArray.getJSONObject(i);
Map<String, Object> rowData = new HashMap<>();
rowData.put("seq", i + 1);
rowData.put("materialName", item.getString("materialName"));
// ... 其他字段
rowsList.add(rowData);
}
dataMap.put("rows", rowsList);
```
#### 3.3 使用配置渲染
```java
XWPFTemplate template = XWPFTemplate.compile(templateStream, config)
.render(dataMap);
```
## 🎯 完整的Word模板示例
### 模板结构
```
服务合同
需方:{{clientName}} 合同编号:{{contractCode}}
供方:上海诺力智能科技有限公司 签订时间:{{effectiveDate}}
一、产品明细单
┌─────────┬──────────┬──────────┬──────┬──────┬──────────┬──────────┬──────┐
│ 序号 │ 产品名称 │ 型号 │ 数量 │ 单位 │ 单价(元) │ 总价(元) │ 备注 │
├─────────┼──────────┼──────────┼──────┼──────┼──────────┼──────────┼──────┤
│{{#rows}}│ │ │ │ │ │ │ │
├─────────┼──────────┼──────────┼──────┼──────┼──────────┼──────────┼──────┤
│{{seq}} │{{materialName}}│{{materialSpec}}│{{qty}}│{{unitName}}│{{salePrice}}│{{amount}}│{{remark}}│
├─────────┼──────────┼──────────┼──────┼──────┼──────────┼──────────┼──────┤
│{{/rows}}│ │ │ │ │ │ │ │
├─────────┴──────────┴──────────┴──────┴──────┴──────────┼──────────┼──────┤
│ 共计: │{{totalPrice}}│ │
└──────────────────────────────────────────────┴──────────┴──────┘
二、质量要求:{{qc}}
三、交货时间、地点:货期:{{delivery}},交货地:{{place}}
四、运输方式:{{transport}}
五、包装标准:{{packaging}}
六、结算方式:{{pay}},付款方式:{{payment}}
七、违约责任:{{breach}}
八、解决合同纠纷的方式:{{solve_dispute}}
九、其它约定事项:{{supplement}}
┌──────────────────────────────┬──────────────────────────────┐
│ 需方: │ 供方: │
├──────────────────────────────┼──────────────────────────────┤
│单位名称:{{clientName}} │单位名称:上海诺力智能科技有限公司│
│地址:{{clientAdd}} │地址:上海青浦区徐泾镇... │
│委托代理人:{{juridicalPerson}}│委托代理人: │
│电话:{{clientTel}} │电话: │
│传真:{{clientFax}} │传真: │
│开户银行:{{clientBank}} │开户银行:招商银行虹桥支行 │
│帐号:{{clientCard}} │帐号12191702501091 │
└──────────────────────────────┴──────────────────────────────┘
```
## 📝 字段说明
### 基本信息字段
| 字段名 | 说明 | 示例 |
|--------|------|------|
| clientName | 客户名称 | XX公司 |
| contractCode | 合同编号 | HT20250101 |
| effectiveDate | 生效日期 | 2025年01月01日 |
| totalPrice | 总价 | 100000.00 |
### 列表字段rows
| 字段名 | 说明 | 示例 |
|--------|------|------|
| seq | 序号 | 1, 2, 3... |
| materialName | 产品名称 | 叉车 |
| materialSpec | 型号 | CPD15 |
| qty | 数量 | 10 |
| unitName | 单位 | 台 |
| salePrice | 单价 | 50000.00 |
| amount | 总价 | 500000.00 |
| remark | 备注 | 含税 |
### 合同条款字段
| 字段名 | 说明 |
|--------|------|
| qc | 质量要求 |
| delivery | 交货时间 |
| place | 交货地点 |
| transport | 运输方式 |
| packaging | 包装标准 |
| pay | 结算方式 |
| payment | 付款方式 |
| breach | 违约责任 |
| solve_dispute | 解决纠纷方式 |
| supplement | 其它约定 |
### 客户信息字段
| 字段名 | 说明 |
|--------|------|
| clientAdd | 客户地址 |
| juridicalPerson | 法人代表 |
| clientTel | 客户电话 |
| clientFax | 客户传真 |
| clientBank | 客户开户行 |
| clientCard | 客户账号 |
## 🔧 常见问题
### 1. 列表数据不显示
**原因**:没有配置 `LoopRowTableRenderPolicy`
**解决**:在代码中添加配置
```java
Configure config = Configure.builder()
.bind("rows", policy)
.build();
```
### 2. 列表只显示一行
**原因**:模板中 `{{#rows}}``{{/rows}}` 没有单独占一行
**解决**:确保循环标签单独占表格的一行
### 3. 数据格式错误
**原因**直接传JSONArray而不是List<Map>
**解决**转换为List<Map<String, Object>>格式
### 4. 中文乱码
**原因**:文件名编码问题
**解决**使用URLEncoder编码文件名
```java
String fileName = java.net.URLEncoder.encode("合同.docx", "UTF-8");
```
## 🎉 测试步骤
1. **准备模板文件**
- 按照上述格式创建Word模板
- 保存为 `contract_template.docx`
- 放到 `src/main/resources/templates/` 目录
2. **测试导出**
- 访问:`GET /flow/contract/export?contractId=1`
- 检查下载的Word文档
- 验证列表数据是否正确显示
3. **验证数据**
- 打开导出的Word文档
- 检查产品明细表是否有多行数据
- 检查所有字段是否正确填充
## 💡 最佳实践
1. **模板管理**
- 将模板文件放在resources目录
- 使用版本控制管理模板
- 为不同类型合同创建不同模板
2. **错误处理**
- 添加try-catch捕获异常
- 记录详细的错误日志
- 返回友好的错误提示
3. **性能优化**
- 缓存模板对象
- 使用流式处理大文件
- 异步处理导出任务
## 📚 参考资料
- poi-tl官方文档http://deepoove.com/poi-tl/
- 循环行表格http://deepoove.com/poi-tl/#_loop-row-table
- GitHub示例https://github.com/Sayi/poi-tl
---
**更新时间**: 2026-01-30
**版本**: v1.0

View File

@@ -71,6 +71,42 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.6</version>
</dependency>
<!--Excel-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.10.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.21</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>

View File

@@ -67,6 +67,8 @@ public class ShiroConfig {
filterMap.put("/js/**", "anon");
filterMap.put("/img/**", "anon");
filterMap.put("/fonts/**", "anon");
filterMap.put("/fonts/**", "anon");
filterMap.put("/flow/contract/export", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);

View File

@@ -1,5 +1,7 @@
package com.boge.modules.contract.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -10,26 +12,31 @@ import com.boge.common.utils.CodeUtil;
import com.boge.common.utils.PageUtils;
import com.boge.common.utils.R;
import com.boge.common.utils.ShiroUtils;
import com.boge.modules.client.entity.ClientQuery;
import com.boge.modules.contract.dao.ContractDao;
import com.boge.modules.client.entity.ClientEntity;
import com.boge.modules.client.service.ClientService;
import com.boge.modules.contract.entity.ContractEntity;
import com.boge.modules.contract.entity.ContractQuery;
import com.boge.modules.contract.service.ContractService;
import com.boge.modules.orderRecord.record.RecordEvent;
import com.boge.modules.price.entity.PriceEntity;
import com.boge.modules.price.enums.PriceApprovalEnum;
import com.boge.modules.tickets.entity.LocalStorage;
import com.boge.modules.tickets.service.LocalStorageService;
import lombok.val;
import com.boge.modules.sys.localStorage.service.entity.LocalStorage;
import com.boge.modules.sys.localStorage.service.LocalStorageService;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.data.Rows;
import com.deepoove.poi.data.TableRenderData;
import com.deepoove.poi.data.Tables;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@@ -46,6 +53,8 @@ public class ContractController {
@Autowired
private ContractService contractService;
@Autowired
private ClientService clientService;
@Autowired
private LocalStorageService localStorageService;
@Autowired
private EventPublisher eventPublisher;
@@ -132,4 +141,77 @@ public class ContractController {
return R.ok();
}
/**
* 导出合同
*/
@RequestMapping("/export")
//@RequiresPermissions("flow:contract:delete")
public void export(Integer contractId, HttpServletResponse response){
try {
// 1. 查询合同数据
ContractEntity contract = contractService.selectDtlById(contractId);
String materialJson = contract.getMaterialJson();
ClientEntity clientEntity = clientService.getById(contract.getClientId());
// 2. 构建数据Map
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("clientName", clientEntity.getClientName());
dataMap.put("contractCode", contract.getContractCode());
dataMap.put("effectiveDate", new SimpleDateFormat("yyyy年MM月dd日").format(contract.getEffectiveTime()));
dataMap.put("clientAdd", clientEntity.getAddress());
dataMap.put("juridicalPerson", clientEntity.getJuridicalPerson());
dataMap.put("clientTel", clientEntity.getTel());
dataMap.put("clientFax", clientEntity.getFax());
dataMap.put("clientBank", clientEntity.getBank());
dataMap.put("clientCard", clientEntity.getCard());
dataMap.put("totalPrice", contract.getTotalPrice());
dataMap.put("qc", contract.getQc());
dataMap.put("delivery", contract.getDelivery());
dataMap.put("place", contract.getPlace());
dataMap.put("transport", contract.getTransport());
dataMap.put("packaging", contract.getPackaging());
dataMap.put("pay", contract.getPay());
dataMap.put("payment", contract.getPayment());
dataMap.put("breach", contract.getBreach());
dataMap.put("solve_dispute", contract.getSolveDispute());
dataMap.put("supplement", contract.getSupplement());
// 3. 处理产品明细列表数据
JSONArray materialArray = JSONArray.parseArray(materialJson);
List<Map<String, Object>> rowsList = new ArrayList<>();
for (int i = 0; i < materialArray.size(); i++) {
JSONObject item = materialArray.getJSONObject(i);
Map<String, Object> rowData = new HashMap<>();
rowData.put("seq", i + 1); // 序号
rowData.put("materialName", item.getString("materialName"));
rowData.put("materialSpec", item.getString("materialSpec"));
rowData.put("qty", item.getString("qty"));
rowData.put("unitName", item.getString("unitName"));
rowData.put("salePrice", item.getString("salePrice"));
rowData.put("amount", item.getString("amount"));
rowData.put("remark", item.getString("remark"));
rowsList.add(rowData);
}
dataMap.put("goods", rowsList);
// 4. 配置循环行渲染策略
com.deepoove.poi.config.Configure config = com.deepoove.poi.config.Configure.builder()
.bind("goods", new LoopRowTableRenderPolicy()) // 绑定rows标签使用循环行策略
.build();
InputStream templateStream = new java.io.FileInputStream("/Users/mima0000/Desktop/合同.docx");
XWPFTemplate template = XWPFTemplate.compile(templateStream, config).render(dataMap);
// 6. 设置响应头
String fileName = "合同_" + contract.getContractCode() + ".docx";
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" +
java.net.URLEncoder.encode(fileName, "UTF-8"));
// 7. 输出到响应流
template.write(response.getOutputStream());
template.close();
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException("导出合同失败: " + ex.getMessage());
}
}
}

View File

@@ -13,8 +13,8 @@ import com.boge.modules.flow.entity.FlwHiTaskEntity;
import com.boge.modules.flow.service.ActHiProcessinfoService;
import com.boge.modules.flow.service.FlwInstanceService;
import com.boge.modules.sys.entity.SysUserEntity;
import com.boge.modules.tickets.entity.LocalStorage;
import com.boge.modules.tickets.service.LocalStorageService;
import com.boge.modules.sys.localStorage.service.entity.LocalStorage;
import com.boge.modules.sys.localStorage.service.LocalStorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.*;

View File

@@ -1,8 +1,8 @@
package com.boge.modules.price.controller;
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 com.boge.common.publisher.EventPublisher;
import com.boge.common.publisher.EventType;
@@ -11,18 +11,18 @@ import com.boge.common.utils.PageUtils;
import com.boge.common.utils.R;
import com.boge.common.utils.ShiroUtils;
import com.boge.modules.orderRecord.record.RecordEvent;
import com.boge.modules.orderRecord.record.service.PmOrderRecordService;
import com.boge.modules.price.entity.PriceDto;
import com.boge.modules.price.entity.PriceEntity;
import com.boge.modules.price.enums.PriceApprovalEnum;
import com.boge.modules.price.service.PriceService;
import com.boge.modules.tickets.entity.LocalStorage;
import com.boge.modules.tickets.service.LocalStorageService;
import com.boge.modules.sys.localStorage.service.entity.LocalStorage;
import com.boge.modules.sys.localStorage.service.LocalStorageService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -128,4 +128,16 @@ public class PriceController {
return R.ok();
}
/**
* 审核
*/
@RequestMapping("/export")
//@RequiresPermissions("flow:contract:delete")
public R export(Integer priceId, HttpServletResponse response){
PriceEntity contract = priceService.getById(priceId);
ClassPathResource pathResource = new ClassPathResource("static/model/报价单样式.xls");// 根目录
String materialJson = contract.getMaterialJson();
localStorageService.downloadExcelModel(pathResource.getPath(),response,(JSONObject)JSON.toJSON(contract),JSONArray.parseArray(materialJson));
return R.ok();
}
}

View File

@@ -2,12 +2,12 @@
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.boge.modules.sys;
package com.boge.modules.sys.localStorage.controller;
import com.boge.common.exception.RRException;
import com.boge.common.utils.FileUtil;
import com.boge.modules.tickets.entity.LocalStorage;
import com.boge.modules.tickets.service.LocalStorageService;
import com.boge.modules.sys.localStorage.service.LocalStorageService;
import com.boge.modules.sys.localStorage.service.entity.LocalStorage;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;

View File

@@ -1,14 +1,14 @@
package com.boge.modules.tickets.service;
package com.boge.modules.sys.localStorage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.boge.modules.tickets.entity.LocalStorage;
import com.boge.modules.tickets.entity.TicketsEntity;
import com.boge.modules.sys.localStorage.service.entity.LocalStorage;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public interface LocalStorageService extends IService<LocalStorage> {
@@ -21,4 +21,14 @@ public interface LocalStorageService extends IService<LocalStorage> {
void download(List<LocalStorage> localStorageDtos, HttpServletResponse response) throws IOException;
void downloadFile(LocalStorage localStorage, HttpServletRequest request, HttpServletResponse response) throws IOException;
/**
*
* @param fileUrl
* @param response
* @param mapParam:模版中json的数据{xxx}
* @param listParam模版中列表的数据{date.xxx}
*/
void downloadExcelModel(String fileUrl, HttpServletResponse response, Map mapParam, List listParam);
void downloadWordModel(String fileUrl, HttpServletResponse response, Map mapParam);
}

View File

@@ -1,8 +1,7 @@
package com.boge.modules.tickets.dao;
package com.boge.modules.sys.localStorage.service.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.boge.modules.tickets.entity.LocalStorage;
import com.boge.modules.tickets.entity.TicketsEntity;
import com.boge.modules.sys.localStorage.service.entity.LocalStorage;
import org.apache.ibatis.annotations.Mapper;
@Mapper

View File

@@ -0,0 +1,15 @@
package com.boge.modules.sys.localStorage.service.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class LocalStorageDto implements Serializable {
private Long id;
private String realName;
private String name;
private String suffix;
private String type;
private String size;
}

View File

@@ -1,19 +1,11 @@
package com.boge.modules.tickets.entity;
package com.boge.modules.sys.localStorage.service.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.elasticsearch.annotations.Document;
import javax.persistence.*;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Date;

View File

@@ -1,4 +1,4 @@
package com.boge.modules.tickets.entity;
package com.boge.modules.sys.localStorage.service.entity;
import java.sql.Timestamp;
import java.util.List;

View File

@@ -1,29 +1,41 @@
package com.boge.modules.tickets.service.impl;
package com.boge.modules.sys.localStorage.service.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import java.io.File;
import java.io.IOException;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.boge.common.exception.RRException;
import com.boge.common.query.MapOf;
import com.boge.common.utils.FileProperties;
import com.boge.common.utils.FileUtil;
import com.boge.modules.contract.entity.ContractEntity;
import com.boge.modules.contract.service.ContractService;
import com.boge.modules.tickets.dao.LocalStorageMapper;
import com.boge.modules.tickets.entity.LocalStorage;
import com.boge.modules.tickets.service.LocalStorageService;
import com.boge.modules.sys.localStorage.service.LocalStorageService;
import com.boge.modules.sys.localStorage.service.dao.LocalStorageMapper;
import com.boge.modules.sys.localStorage.service.entity.LocalStorage;
import com.deepoove.poi.XWPFTemplate;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindException;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;
@Service
public class LocalStorageServiceImpl extends ServiceImpl<LocalStorageMapper, LocalStorage> implements LocalStorageService {
@@ -101,4 +113,56 @@ public class LocalStorageServiceImpl extends ServiceImpl<LocalStorageMapper, Lo
FileUtil.downloadFile(request, response,file,false);
}
@Override
public void downloadExcelModel(String fileUrl, HttpServletResponse response, Map mapParam, List listParam){
ExcelWriter writer;
InputStream templateInputStream = null;
try {
ClassPathResource classPathResource = new ClassPathResource(fileUrl);
if (!classPathResource.exists()){
throw new RRException("文件不存在"+fileUrl);
}
templateInputStream = classPathResource.getInputStream();
// templateInputStream = new FileInputStream("/Users/mima0000/Desktop/报价单.xlsx");
response.setHeader("Content-Disposition", "attachment; filename*=UTf-8''"+"12321"+".xlsx");
ServletOutputStream outputStream = response.getOutputStream();
writer = EasyExcel.write(outputStream).withTemplate(templateInputStream).build();
WriteSheet sheet = EasyExcel.writerSheet().build();
writer.fill(mapParam, sheet);
if (!CollectionUtils.isEmpty(listParam)){
writer.fill(new FillWrapper("data", listParam), sheet);
}
writer.finish();
outputStream.close();
}catch (Exception ex){
ex.printStackTrace();
throw new RRException(ex.getMessage());
}finally {
// ExcelWriter writer;
// InputStream templateInputStream;
try{
if (templateInputStream!=null) templateInputStream.close();
}catch (Exception ex){}
}
}
public void downloadWordModel(String fileUrl, HttpServletResponse response, Map mapParam){
// 2. 加载模板并填充类似 EasyExcel
ClassPathResource classPathResource = new ClassPathResource(fileUrl);
// if (!classPathResource.exists()){
// throw new RRException("文件不存在"+fileUrl);
// }
InputStream inputStream = null;
XWPFTemplate template = null;
try {
// 3. 输出到文件
template = XWPFTemplate.compile("/Users/mima0000/Desktop/合同.docx").render(mapParam);
template.writeAndClose(new FileOutputStream("/Users/mima0000/Desktop/"+new Random().nextInt(1000)+"23.docx"));
}catch (Exception ex){
ex.printStackTrace();
}
}
}

View File

@@ -1,87 +0,0 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.boge.modules.tickets.controller;
import com.boge.common.exception.RRException;
import com.boge.common.utils.FileUtil;
import com.boge.modules.tickets.entity.LocalStorage;
import com.boge.modules.tickets.service.LocalStorageService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@Api(
tags = {"工具:本地存储管理"}
)
@RequestMapping({"/api/localStorage"})
public class LocalStorageController {
private final LocalStorageService localStorageService;
@ApiOperation("查询文件")
@GetMapping
public ResponseEntity<Object> query(@RequestParam String annex) {
List<LocalStorage> list = new ArrayList<>();
if (StringUtils.isNotEmpty(annex)){
String[] split = annex.split(",");
list = localStorageService.listByIds(Arrays.asList(split));
}
return new ResponseEntity(list, HttpStatus.OK);
}
@ApiOperation("导出数据")
@GetMapping({"/download"})
public void download(@RequestParam Long storageId , HttpServletResponse response, HttpServletRequest request) throws IOException {
this.localStorageService.downloadFile(this.localStorageService.getById(storageId),request, response);
}
@ApiOperation("上传文件")
@PostMapping
public ResponseEntity<Object> create(@RequestParam String name, @RequestParam("file") MultipartFile file) {
LocalStorage localStorage = this.localStorageService.create(name, file);
return new ResponseEntity(localStorage, HttpStatus.CREATED);
}
@PostMapping({"/pictures"})
@ApiOperation("上传图片")
public ResponseEntity<Object> upload(@RequestParam MultipartFile file) {
String suffix = FileUtil.getExtensionName(file.getOriginalFilename());
if (!"图片".equals(FileUtil.getFileType(suffix))) {
throw new RRException("只能上传图片");
} else {
LocalStorage localStorage = this.localStorageService.create((String)null, file);
return new ResponseEntity(localStorage, HttpStatus.OK);
}
}
@DeleteMapping
@ApiOperation("多选删除")
public ResponseEntity<Object> delete(@RequestBody Long[] ids) {
localStorageService.removeByIds(Arrays.asList(ids));
return new ResponseEntity(HttpStatus.OK);
}
public LocalStorageController(final LocalStorageService localStorageService) {
this.localStorageService = localStorageService;
}
}

View File

@@ -8,11 +8,7 @@ import com.boge.common.utils.ShiroUtils;
import com.boge.modules.sys.entity.SysUserEntity;
import com.boge.modules.tickets.dto.TicketsDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import com.boge.modules.tickets.entity.TicketsEntity;
import com.boge.modules.tickets.service.TicketsService;
@@ -78,6 +74,16 @@ public class TicketsController {
ticketsService.saveTicket(tickets);
return R.ok();
}
/**
* 发起
*/
@GetMapping("/initiate")
//@RequiresPermissions("tickets:tickets:save")
public R initiate( String ticketsId){
ticketsService.initiate(ticketsId);
return R.ok();
}
/**
* 修改

View File

@@ -10,7 +10,8 @@ import lombok.Getter;
UNCHECK(0, "未开始"),
CHECKED(1, "已指派"),
REJECT(2, "处理中"),
FINISH(3, "已完成");
FINISH(3, "已完成"),
REFUSE(4, "拒绝");
private Integer code;
private String msg;

View File

@@ -23,5 +23,7 @@ public interface TicketsService extends IService<TicketsEntity> {
PageUtils queryPageByType(Map<String, Object> params);
void saveTicket(TicketsEntity tickets);
void initiate(String ticketsId);
}

View File

@@ -3,6 +3,8 @@ package com.boge.modules.tickets.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.boge.common.exception.RRException;
import com.boge.common.utils.ShiroUtils;
import com.boge.modules.car.entity.CarEntity;
import com.boge.modules.car.service.CarService;
@@ -13,6 +15,7 @@ import com.boge.modules.sys.service.SysUserRoleService;
import com.boge.modules.sys.service.impl.SysUserServiceImpl;
import com.boge.modules.tickets.dto.TicketsDTO;
import com.boge.modules.tickets.enums.TicketUserEnums;
import org.apache.commons.lang3.StringUtils;
import org.flowable.common.engine.api.FlowableException;
import org.flowable.engine.IdentityService;
import org.flowable.engine.RuntimeService;
@@ -115,22 +118,27 @@ public class TicketsServiceImpl extends ServiceImpl<TicketsDao, TicketsEntity> i
@Override
public void saveTicket(TicketsEntity tickets) {
// 启动流程
try {
// 记录流程的发起人
identityService.setAuthenticatedUserId(ShiroUtils.getUserId().toString());
Map<String, Object> startVars = new HashMap<>();
ProcessInstance processInstance = runtimeService.startProcessInstanceById(defId, startVars);
SysUserEntity loginUser = ShiroUtils.getUserEntity();
tickets.setCreateTime(new Date());
tickets.setCreateUser(loginUser.getNickname());
tickets.setStatus(TicketsStatusEnums.REJECT.getCode());
tickets.setAssignUserId(TicketUserEnums.SPECIALIST.getCode());
tickets.setProcessInstance(processInstance.getProcessInstanceId());
ticketsDao.insert(tickets);
} catch (Exception e) {
e.printStackTrace();
SysUserEntity loginUser = ShiroUtils.getUserEntity();
tickets.setCreateTime(new Date());
tickets.setCreateUser(loginUser.getNickname());
tickets.setStatus(TicketsStatusEnums.UNCHECK.getCode());
ticketsDao.insert(tickets);
}
@Override
public void initiate(String ticketsId) {
if (StringUtils.isEmpty(ticketsId)){
throw new RRException("工单ID不能为空");
}
// 记录流程的发起人
identityService.setAuthenticatedUserId(ShiroUtils.getUserId().toString());
Map<String, Object> startVars = new HashMap<>();
ProcessInstance processInstance = runtimeService.startProcessInstanceById(defId, startVars);
this.update(new LambdaUpdateWrapper<TicketsEntity>()
.set(TicketsEntity::getProcessInstance,processInstance.getProcessInstanceId())
.set(TicketsEntity::getStatus,TicketsStatusEnums.REJECT.getCode())
.set(TicketsEntity::getAssignUserId,ShiroUtils.getUserId())
.eq(TicketsEntity::getTicketsId,ticketsId));
}
@Override

Binary file not shown.

Binary file not shown.

View File

@@ -52,19 +52,14 @@
</span>
</div>
<div class="clause-item">
<strong> 售后服务:</strong>
<span v-text="dataForm.after_sales" @input="this.dataForm.after_sales = $event.target.innerText" class="editable-clause" contenteditable="true" style="text-decoration: underline;">
</span>
</div>
<div class="clause-item">
<strong> 交货时间地点:</strong>
<strong> 服务时间地点:</strong>
货期:
<span v-text="dataForm.delivery" @input="this.dataForm.delivery = $event.target.innerText" class="editable-clause" contenteditable="true" style="text-decoration: underline;"/>
交货地:
<span v-text="dataForm.place" @input="this.dataForm.place = $event.target.innerText" class="editable-clause" contenteditable="true" style="text-decoration: underline;"/>
</div>
<div class="clause-item">
<strong> 运输方式及到达站和费用负担:</strong>
<strong> 运输方式及到达站和费用负担:</strong>
<span v-text="dataForm.transport" @input="this.dataForm.transport = $event.target.innerText" class="editable-clause" contenteditable="true" style="text-decoration: underline;"/>
</div>
<div class="clause-item">
@@ -285,193 +280,33 @@ export default {
return result
},
exportToExcel () {
// 获取客户名称
let clientName = ''
if (this.dictData && this.dictData[1]) {
const client = this.dictData[1].find(item => item.value === this.dataForm.clientId)
clientName = client ? client.label : ''
// 检查contractId是否存在
if (!this.dataForm.contractId) {
this.$message.error('合同ID不存在无法导出')
return
}
// 构建Excel HTML内容
let excelHtml = `
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta charset="UTF-8">
<!--[if gte mso 9]>
<xml>
<x:ExcelWorkbook>
<x:ExcelWorksheets>
<x:ExcelWorksheet>
<x:Name>产品购销合同</x:Name>
<x:WorksheetOptions>
<x:DisplayGridlines/>
</x:WorksheetOptions>
</x:ExcelWorksheet>
</x:ExcelWorksheets>
</x:ExcelWorkbook>
</xml>
<![endif]-->
<style>
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #000;
padding: 8px;
text-align: center;
font-size: 12px;
}
th {
background-color: #f5f5f5;
font-weight: bold;
}
.text-left {
text-align: left;
}
.title-row {
font-size: 14px;
font-weight: bold;
padding: 10px 0;
}
</style>
</head>
<body>
<table>
<tr>
<td colspan="8" style="font-size: 18px; font-weight: bold; text-align: center; padding: 15px;">产品购销合同</td>
</tr>
<tr>
<td colspan="4" class="text-left">需方:${clientName}</td>
<td colspan="4" class="text-left">合同编号:${this.dataForm.contractCode || ''}</td>
</tr>
<tr>
<td colspan="4" class="text-left">供方:上海诺力智能科技有限公司</td>
<td colspan="4" class="text-left">生效日期:${this.dataForm.effectiveTime || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left title-row">一、产品明细单</td>
</tr>
<tr>
<th>序号</th>
<th>产品名称</th>
<th>订货代码</th>
<th>型号</th>
<th>数量</th>
<th>单位</th>
<th>单价(元)</th>
<th>总价(元)</th>
</tr>
`
// 添加产品明细
this.materData.forEach((item, index) => {
excelHtml += `
<tr>
<td>${index + 1}</td>
<td>${item.materialName || ''}</td>
<td>${item.materialCode || ''}</td>
<td>${item.materialSpec || ''}</td>
<td>${item.qty || ''}</td>
<td>${item.unitName || ''}</td>
<td>${item.salePrice || ''}</td>
<td>${item.amount || ''}</td>
</tr>
`
// 调用后端接口导出Excel
const url = this.$http.adornUrl('/flow/contract/export')
const params = this.$http.adornParams({
contractId: this.dataForm.contractId
})
// 添加合计行
excelHtml += `
<tr>
<td colspan="7" class="text-left">共计:</td>
<td>${this.dataForm.totalPrice || ''}</td>
</tr>
<tr>
<td colspan="6" class="text-left">共计人民币金额:(大写)${this.toChineseCurrency(this.dataForm.totalPrice)}</td>
<td colspan="2">含13%增值税</td>
</tr>
`
// 构建完整的URL
const fullUrl = `${url}?${new URLSearchParams(params).toString()}`
// 添加合同条款
excelHtml += `
<tr>
<td colspan="8" class="text-left">二、质量要求、技术标准、供方对质量负责的条件和期限:${this.dataForm.qc || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">三、售后服务:${this.dataForm.afterSales || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">四、交货时间、地点:货期:${this.dataForm.delivery || ''},交货地:${this.dataForm.place || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">五、运输方式及到达站和费用负担:${this.dataForm.transport || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">六、包装标准:${this.dataForm.packaging || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">七、结算方式:${this.dataForm.pay || ''},付款方式:${this.dataForm.payment || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">八、违约责任:${this.dataForm.breach || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">九、解决合同纠纷的方式:${this.dataForm.solveDispute || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">十、其它约定事项:${this.dataForm.supplement || ''}</td>
</tr>
`
// 创建隐藏的iframe来下载文件
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = fullUrl
document.body.appendChild(iframe)
// 添加双方信息
excelHtml += `
<tr>
<td colspan="4" class="text-left" style="vertical-align: top;">
<div><strong>需方:</strong></div>
<div>单位名称:${this.client.clientName || ''}</div>
<div>地址:${this.client.address || ''}</div>
<div>委托代理电话:${this.client.tel || ''}</div>
<div>传真:${this.client.fax || ''}</div>
<div>开户银行:${this.client.bank || ''}</div>
<div>帐号:${this.client.card || ''}</div>
</td>
<td colspan="4" class="text-left" style="vertical-align: top;">
<div><strong>供方:</strong></div>
<div>单位名称:上海诺力智能科技有限公司(盖章)</div>
<div>地址上海青浦区徐泾镇高光路215弄99号4号楼302室</div>
<div>委托代理电话:</div>
<div>传真:</div>
<div>开户银行:招商银行虹桥支行</div>
<div>帐号12191702501091</div>
</td>
</tr>
</table>
</body>
</html>
`
// 延迟移除iframe
setTimeout(() => {
document.body.removeChild(iframe)
}, 3000)
// 创建Blob对象
const blob = new Blob(['\ufeff', excelHtml], {
type: 'application/vnd.ms-excel;charset=utf-8'
})
// 生成文件名
const fileName = `产品购销合同_${this.dataForm.contractCode || new Date().getTime()}.xls`
// 创建下载链接
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = fileName
// 触发下载
document.body.appendChild(link)
link.click()
// 清理
document.body.removeChild(link)
URL.revokeObjectURL(link.href)
this.$message.success('Excel文件导出成功')
this.$message.success('正在导出Excel文件请稍候...')
}
}
}

View File

@@ -151,12 +151,26 @@
fixed="right"
header-align="center"
align="center"
width="170"
width="220"
label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.ticketsId)">修改</el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.ticketsId)">删除</el-button>
<el-button type="text" size="small" @click="startFlowHandle(scope.row.processInstance)">流程进度</el-button>
<el-button
v-if="scope.row.status === 0"
type="text" size="small" @click="addOrUpdateHandle(scope.row.ticketsId)">修改</el-button>
<el-button
v-if="scope.row.status === 0"
type="text" size="small" @click="deleteHandle(scope.row.ticketsId)">删除</el-button>
<!-- 只有未开始状态才显示发起流程按钮 -->
<el-button
v-if="scope.row.status === 0"
type="text"
size="small"
@click="initiateFlowHandle(scope.row.ticketsId)">
发起流程
</el-button>
<el-button
v-if="scope.row.status !== 0"
type="text" size="small" @click="startFlowHandle(scope.row.processInstance)">流程进度</el-button>
<el-button type="text" size="small" @click="$router.push(`/tickets-detail?id=${scope.row.ticketsId}`)">详情</el-button>
</template>
</el-table-column>
@@ -389,6 +403,37 @@
this.$message.error(data.msg)
}
})
},
// 发起流程
initiateFlowHandle(ticketsId) {
this.$confirm('确定要发起流程吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/tickets/tickets/initiate'),
method: 'get',
params: this.$http.adornParams({
ticketsId: ticketsId
})
}).then(({data}) => {
if (data && data.code === 200) {
this.$message({
message: '流程发起成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList()
}
})
} else {
this.$message.error(data.msg || '流程发起失败')
}
}).catch(() => {
this.$message.error('流程发起失败,请稍后重试')
})
}).catch(() => {})
}
}
}