Java 字符串根据宽度(像素)换行
在一些场景下,我们经常会通过判断字符串的长度,比如个数来实现换行,但是中文、英文、数字、其实在展示的时候同样长度的字符串,其实它的宽度是不一样的,这也是们我通俗意义上说的宽度(像素)
根据像素宽度进行换行
需求:
/** * 10、自己做图片 ,根据文本宽度进行换行 */ @Test public void creatMyImage(){ //整体图合成 BufferedImage bufferedImage = new BufferedImage(500, 500, BufferedImage.TYPE_INT_RGB); //设置图片的背景色 Graphics2D main = bufferedImage.createGraphics(); main.fillRect(0, 0, 500, 500); String text = "111122223所以比传统纸巾更环保3334441比传统纸巾更环11111111111111122223所以比传统纸巾更环保3334441比传统纸巾更环11111111111111122223所以比传统纸巾更环保3334441比传统纸巾更环11111111111111122223所以比传统纸巾更环保3334441比传统纸巾更环11111111111"; Graphics2D textG = bufferedImage.createGraphics() ; textG.setColor(new Color(37,37,37)); Font hualaoContentFont = new Font("PingFang SC", Font.PLAIN, 20); textG.setFont(hualaoContentFont); textG.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); drawString(textG,text,30,100,4,10,50,true,false); //存储到本地 String saveFilePath = "/Users/healerjean/Desktop/new.png"; saveImageToLocalDir(bufferedImage, saveFilePath); } /** * * @param g * @param text 文本 * @param lineHeight 行高(注意字体大小的控制哦) * @param maxWidth 行宽 * @param maxLine 最大行数 * @param left 左边距 //整段文字的左边距 * @param top 上边距 //整顿文字的上边距 * @param trim 是否修剪文本(1、去除首尾空格,2、将多个换行符替换为一个) * @param lineIndent 是否首行缩进 */ public static void drawString(Graphics2D g, String text, float lineHeight, float maxWidth, int maxLine, float left, float top, boolean trim, boolean lineIndent) { if (text == null || text.length() == 0) return; if(trim) { text = text.replaceAll("\\n+", "\n").trim(); } if(lineIndent) { text = " " + text.replaceAll("\\n", "\n "); } drawString(g, text, lineHeight, maxWidth, maxLine, left, top); } /** * * @param g * @param text 文本 * @param lineHeight 行高 * @param maxWidth 行宽 * @param maxLine 最大行数 * @param left 左边距 * @param top 上边距 */ private static void drawString(Graphics2D g, String text, float lineHeight, float maxWidth, int maxLine, float left, float top) { if (text == null || text.length() == 0) return; FontMetrics fm = g.getFontMetrics(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); sb.append(c); int stringWidth = fm.stringWidth(sb.toString()); if (c == '\n' || stringWidth > maxWidth) { if(c == '\n') { i += 1; } if (maxLine > 1) { g.drawString(text.substring(0, i), left, top); drawString(g, text.substring(i), lineHeight, maxWidth, maxLine - 1, left, top + lineHeight); } else { g.drawString(text.substring(0, i - 1) + "…", left, top); } return; } } g.drawString(text, left, top); }
Java字符串根据宽度(像素)进行换行及Pdf合并
实际开发中,我们经常会通过判断字符串的长度,比如个数来实现换行,但是中文、英文、数字在展示的时候同样长度的字符串,其实它的宽度是不一样的,这也是们我通俗意义上说的宽度(像素)。
下面结合jasper套打打印业务来说明。
1、工具类最终版前奏
package main; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import javax.imageio.ImageIO; import javax.imageio.stream.ImageOutputStream; public class SubstringStr { /** * 1、自己做图片,根据文本宽度进行换行 */ public void creatMyImage(String text) { // 整体图合成 BufferedImage bufferedImage = new BufferedImage(595, 842, BufferedImage.TYPE_INT_RGB); // 设置图片的背景色 Graphics2D main = bufferedImage.createGraphics(); main.fillRect(0, 0, 1500, 2600); Graphics2D graphics2d = bufferedImage.createGraphics(); graphics2d.setColor(new Color(37, 37, 37)); Font hualaoContentFont = new Font("宋体", Font.PLAIN, 20); graphics2d.setFont(hualaoContentFont); graphics2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); drawString1(graphics2d, text, 30, 500, 28, 71, 20, false, false, 0); // 存储到本地 String saveFilePath = "D:\\new.png"; saveImageToLocalDir(bufferedImage, saveFilePath); } private void saveImageToLocalDir(BufferedImage img, String saveFilePath) { try { //bufferedimage 转换成 inputstream ByteArrayOutputStream bs = new ByteArrayOutputStream(); ImageOutputStream imOut = ImageIO.createImageOutputStream(bs); ImageIO.write(img, "jpg", imOut); InputStream inputStream = new ByteArrayInputStream(bs.toByteArray()); long length = imOut.length(); OutputStream outStream = new FileOutputStream(saveFilePath); //输出流 byte[] bytes = new byte[1024]; long count = 0; while(count < length){ int len = inputStream.read(bytes, 0, 1024); count +=len; outStream.write(bytes, 0, len); } outStream.flush(); inputStream.close(); outStream.close(); } catch (Exception e) { e.printStackTrace(); } } /** * @param graphics2d * @param text 文本 * @param lineHeight 行高(注意字体大小的控制哦) * @param maxWidth 行宽 * @param maxLine 最大行数 * @param left 左边距 //整段文字的左边距 * @param top 上边距 //整段文字的上边距 * @param trim 是否修剪文本(1、去除首尾空格,2、将多个换行符替换为一个) * @param lineIndent 是否首行缩进 */ public static void drawString1(Graphics2D graphics2d, String text, float lineHeight, float maxWidth, int maxLine, float left, float top, boolean trim, boolean lineIndent, int resultLine) { if (text == null || text.length() == 0) return; if (trim) { text = text.replaceAll("\\n+", "\n").trim(); System.err.println("执行了。。。。"); } if (lineIndent) { text = " " + text.replaceAll("\\n", "\n "); System.err.println("执行了====="); } drawString2(graphics2d, text, lineHeight, maxWidth, maxLine, left, top, resultLine); } /** * * @param graphics2d * @param text 文本 * @param lineHeight 行高 * @param maxWidth 行宽 * @param maxLine 最大行数 * @param left 左边距 * @param top 上边距 */ private static void drawString2(Graphics2D graphics2d, String text, float lineHeight, float maxWidth, int maxLine, float left, float top, int resultLine) { if (text == null || text.length() == 0) { return; } FontMetrics fm = graphics2d.getFontMetrics(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char strChar = text.charAt(i); sb.append(strChar); int stringWidth = fm.stringWidth(sb.toString()); if (strChar == '\n' || stringWidth > maxWidth) { if (strChar == '\n') { i += 1; System.out.println("\n字符串内容为:" + text.substring(0, i)); } if (maxLine > 1) { resultLine = resultLine + 1; System.err.println("\nif中字符串内容为:" + text.substring(0, i)); graphics2d.drawString(text.substring(0, i), left, top); drawString2(graphics2d, text.substring(i), lineHeight, maxWidth, maxLine - 1, left, top + lineHeight, resultLine); } else { System.err.println("\nelse中字符串内容为:" + text.substring(0, i)); graphics2d.drawString(text.substring(0, i - 1) + "…", left, top); } return; } } System.err.println("最后字符串内容为:" + text + "===行数为:" + resultLine); graphics2d.drawString(text, left, top); } public static void main(String[] args) { String text = "111122223所以比传统纸巾更环保3334441比传统纸巾更环11111111111111122223所以比传统纸巾更环保3334441比传统纸巾" + System.getProperty("line.separator") + "更环66666666"+ System.getProperty("line.separator") + "所以比传统纸巾更环保3334441比传统纸巾更环。。。。。。。" + "111111122223所以比传统纸巾更环保3334441比传统纸巾更环11111111111"; SubstringStr test = new SubstringStr(); test.creatMyImage(text); System.out.println("输入图片完成:D:/new.png"); } }
2、工具类最终版
package com.sinosoft.emr.util; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class SubstrUtil { private static int index = 0; /** * 1、自己做图片 ,根据文本宽度进行换行 */ public static Map<String, Object> creatMyImage(String text, Font font, int appointRow, Map<String, Object> map, int rows) { index = 0; // 整体图合成 BufferedImage bufferedImage = new BufferedImage(595, 842, BufferedImage.TYPE_INT_RGB); Graphics2D graphics2d = bufferedImage.createGraphics(); graphics2d.setColor(new Color(37, 37, 37)); graphics2d.setFont(font); graphics2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); map = myDrawString(graphics2d, text, 30, 500, appointRow, 71, 20, map, rows); return map; } /** * 2、根据宽度截取字符串 * @param graphics2d * @param text 文本 * @param lineHeight 行高 * @param maxWidth 行宽 * @param maxLine 最大行数 * @param left 左边距 * @param top 上边距 */ private static Map<String, Object> myDrawString(Graphics2D graphics2d, String text, int lineHeight, int maxWidth, int maxLine, int left, int top, Map<String, Object> map, int rows) { if (text == null || text.length() == 0) { return map; } FontMetrics fm = graphics2d.getFontMetrics(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char strChar = text.charAt(i); sb.append(strChar); int stringWidth = fm.stringWidth(sb.toString()); int strLength = text.substring(0, i).length(); if (strLength >= 53 || strChar == '\n' || stringWidth >= maxWidth) { if (strChar == '\n') { i += 1; // System.out.println("字符串内容为:" + text.substring(0, i)); } if (maxLine > 1) { rows = rows + 1; // System.err.println("if中字符串内容为:" + text.substring(0, i)); char value = ' ', value1 = ' '; try { value = text.substring(i).charAt(0); value1 = text.substring(i - 1).charAt(0); } catch (Exception e) { /*e.printStackTrace();*/ System.err.println("----" + e.getMessage()); } if (isChinesePunctuation(value) && checkCharCN(value1)) { map.put("row" + rows, text.substring(0, i - 1)); myDrawString(graphics2d, text.substring(i - 1), lineHeight, maxWidth, maxLine - 1, left, top + lineHeight, map, rows); } else { map.put("row" + rows, text.substring(0, i)); myDrawString(graphics2d, text.substring(i), lineHeight, maxWidth, maxLine - 1, left, top + lineHeight, map, rows); } index = index + i; map.put("index", index); // System.err.println("---下标" + index); } else { rows = rows + 1; map.put("row" + rows, text.substring(0, i)); // System.err.println("else中字符串内容为:" + text.substring(0, i)); index = index + i; map.put("index", index); } return map; } } return map; } // 2.1 判断字符是否是中文 public static boolean checkCharCN(char c) { String s = String.valueOf(c); String regex = "[\u4e00-\u9fa5]"; Pattern p = Pattern.compile(regex); Matcher m = p.matcher(s); return m.matches(); } // 2.2 判断字符是否是中文标点符号 public static boolean isChinesePunctuation(char c) { Character.UnicodeBlock ub = Character.UnicodeBlock.of(c); if (ub == Character.UnicodeBlock.GENERAL_PUNCTUATION || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS || ub == Character.UnicodeBlock.VERTICAL_FORMS) { return true; } else { return false; } } /** * 3、针对jasper中宽度较短的文本 * @author wanglong * @date 2020年11月10日下午7:12:14 */ public static Map<String, Object> creatShortImage(String text, Font font, int appointRow, Map<String, Object> map, int rows) { index = 0; // 整体图合成 BufferedImage bufferedImage = new BufferedImage(595, 842, BufferedImage.TYPE_INT_RGB); // 设置图片的背景色 Graphics2D main = bufferedImage.createGraphics(); main.fillRect(0, 0, 595, 842); Graphics2D graphics2d = bufferedImage.createGraphics(); graphics2d.setColor(new Color(37, 37, 37)); graphics2d.setFont(font); graphics2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); map = myDrawShortString(graphics2d, text, 30, 230, appointRow, 71, 20, map, rows); return map; } // 3.1 private static Map<String, Object> myDrawShortString(Graphics2D graphics2d, String text, int lineHeight, int maxWidth, int maxLine, int left, int top, Map<String, Object> map, int rows) { if (text == null || text.length() == 0) { return map; } FontMetrics fm = graphics2d.getFontMetrics(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char strChar = text.charAt(i); sb.append(strChar); int stringWidth = fm.stringWidth(sb.toString()); if (strChar == '\n' || stringWidth > maxWidth) { if (strChar == '\n') { i += 1; //System.out.println("字符串内容为:" + text.substring(0, i)); } if (maxLine > 1) { rows = rows + 1; //System.err.println("if中字符串内容为:" + text.substring(0, i)); map.put("row" + rows, text.substring(0, i)); map.put("row_len" + rows, stringWidth); myDrawShortString(graphics2d, text.substring(i), lineHeight, maxWidth, maxLine - 1, left, top + lineHeight, map, rows); } else { rows = rows + 1; map.put("row" + rows, text.substring(0, i)); map.put("row_len" + rows, stringWidth); //System.err.println("else中字符串内容为:" + text.substring(0, i)); } return map; } } return map; } public static void main(String[] args) { Map<String, Object> map = new LinkedHashMap<>(); int rows = 0; int appointRow = 4; Font font = new Font("宋体", Font.PLAIN, 12); String text = "111122223所以比传统纸巾更环保3334441比传统纸巾更环11111111111111122223所以比传统纸巾更环保3334441测试数据" + System.getProperty("line.separator") + "在仿苹果官网垃圾桶时, 设计出的UI使用PingFang SC 字体"+ System.getProperty("line.separator") + "天津市防控指挥部消息称,经排查,滨海新区中新天津纸巾更环保3334441比传统纸巾更环666所以比传统纸巾更环保3334441比传统纸巾更环8888"; int length = text.length(); map = creatMyImage(text, font, appointRow, map, rows); if (length > index) { System.out.println("还剩余文本:" + text.substring(index)); } System.out.println("\n<<--->>>rows=" + rows + ", map="+map); } }
说明:此工具类我们经理进行了改造,生成的文本内容符合中国人的书写习惯:行首不能是标点符号。如果行首是标点符号,上一行的最后一个文字会挪至本行。
3、项目中具体使用
3.1 controller层
/** * 4、打印 病程记录 * @author wanglong * @date 2020年3月4日 下午5:22:57 */ @RequestMapping(value = "printEmrCourse", method = RequestMethod.GET, produces = MediaType.APPLICATION_PDF_VALUE) public void printEmrCourse(@RequestParam(value = "id", required = true) String id, @RequestParam(value = "courseType", required = true) String courseType, @RequestParam(value = "token", required = true) String token, HttpServletResponse response) { List<String> filePathList = new ArrayList<String>(); Map<String, Map<String, Object>> map = iEmrCourseSelectBusi.selectEmrCourseData(id, courseType, token); try { if (map.size() > 0) { for (int i = 0, len = map.size(); i < len; i++) { if (i % 2 == 0) { String path1 = iEmrCourseSelectBusi.getCoursePdf(map.get("map" + i), "emrCourseLeft.jasper"); if (!"".equals(path1)) { filePathList.add(path1); } } else { String path2 = iEmrCourseSelectBusi.getCoursePdf(map.get("map" + i), "emrCourseRight.jasper"); if (!"".equals(path2)) { filePathList.add(path2); } } } iEmrCourseSelectBusi.printEmrCoursePdf(filePathList, response); } } catch (Exception e) { e.printStackTrace(); } }
3.2 serviceImpl层
@Service("iEmrCourseSelectBusi") public class EmrCourseBusiSelectImpl extends EmrCourseSelectServiceImpl implements IEmrCourseSelectBusi{ private static final long serialVersionUID = 1L; private Font font = new Font("宋体", Font.PLAIN, 12); // 读取配置文件里的jasper路径 @Value("${jasperPath}") private String jasperPath; /** * 4、打印 病程记录 * @author wanglong * @date 2020年11月9日下午3:49:23 */ @Override public Map<String, Map<String, Object>> selectEmrCourseData(String id, String courseType, String token) { Map<String, Map<String, Object>> resultMap = new LinkedHashMap<>(); Map<String, Object> map = new LinkedHashMap<>(); int rows = 0, appointRow = 28; StringBuffer bufferStr = new StringBuffer(); String lineBreak = System.getProperty("line.separator"); // 换行符 try { EmrCourse record = emrCourseMapper.selectByPrimaryKey(id); if (record != null) { map.put("patientName", record.getPatientName()); map.put("fkHosRegisterHosNo", record.getFkHosRegisterHosNo()); String recordTime = DateUtil.dateToString(record.getCourseRecordTime(), "yyyy-MM-dd HH:mm"); String doctorName = record.getDoctorRealName(); if ("1".equals(courseType)) { bufferStr.append(recordTime + lineBreak); String tempStr = record.getGeneralRecords() == null ? "" : record.getGeneralRecords(); String blankStr = ""; if (!"".equals(tempStr)) { blankStr = StringDealUtil.getSubString(blankStr, tempStr); } String generalRecords = record.getGeneralRecords() == null ? "" : (blankStr + lineBreak); bufferStr.append(generalRecords); } String updateName = record.getUpdateOprName(); String opraterName = updateName == null ? record.getAddOprName() : updateName; bufferStr.append("\t\t\t\t\t\t\t\t\t签 名:" + opraterName + lineBreak); String text = bufferStr.toString(); int length = text.length(); map = SubstrUtil.creatMyImage(text, font, appointRow, map, rows); map.put("pageNum", "1"); resultMap.put("map0", map); int index = (int) map.get("index"); if (length > index) { String _text = StringDealUtil.getSurplusText(text, index); resultMap = this.getOtherMap(_text, appointRow, resultMap, 1); } } } catch (Exception e) { e.printStackTrace(); } return resultMap; } // 4.1 截取文本获取其余map private Map<String, Map<String, Object>> getOtherMap(String text, int appointRow, Map<String, Map<String, Object>> resultMap, int i) { int length = text.length(); Map<String, Object> map = new LinkedHashMap<>(); map = SubstrUtil.creatMyImage(text, font, appointRow, map, 0); map.put("pageNum", i + 1 + ""); resultMap.put("map"+ i, map); int index = (int) map.get("index"); if (length > index) { String _text = StringDealUtil.getSurplusText(text, index); resultMap = this.getOtherMap(_text, appointRow, resultMap, i+1); } return resultMap; } /** * 5、生成pdf * @author wanglong * @date 2020年11月9日下午1:52:42 */ @Override public String getCoursePdf(Map<String, Object> map, String jasperName) { String fileInfo = ""; Instant timestamp = Instant.now(); try { JRDataSource jrDataSource = new JRBeanCollectionDataSource(null); File reportFile = new File(jasperPath + jasperName); String exportFileName = timestamp.toEpochMilli() + ".pdf"; fileInfo = jasperPath + exportFileName; //调用工具类 JasperHelper.exportPdf2File(jasperPath + exportFileName, reportFile, map, jrDataSource); } catch (Exception e) { e.printStackTrace(); } return fileInfo; } /** * 5.1 合成pdf进行打印 * @author wanglong * @date 2020年11月9日下午1:52:58 */ @Override public void printEmrCoursePdf(List<String> filePathList, HttpServletResponse response) { List<File> files = new ArrayList<File>(); try { if (filePathList != null && filePathList.size() > 0) { for (int i = 0, length = filePathList.size(); i < length; i++) { String pathName = filePathList.get(i); files.add(new File(pathName)); } String newFileName = Instant.now().toEpochMilli() + "病程记录mul2one.pdf"; String targetPath = jasperPath + newFileName; File newFile = PdfMergeUtil.mulFile2One(files, targetPath); if (newFile.exists()) { // 先将单个的pdf文件删除 for (File file : files) { if (file.exists() && file.isFile()) { file.delete(); } } newFile.getName(); PdfUtil.downloadFile(response, targetPath, newFileName); } } } catch (Exception e) { e.printStackTrace(); } } }
4、工具类补充
package utils; import net.sf.jasperreports.engine.*; import net.sf.jasperreports.engine.util.JRLoader; import java.io.*; import java.util.Map; public class JasperHelper { /** * 1、直接导出pdf文件 * @author wanglong * @date 2020年10月31日下午9:10:41 */ public static void exportPdf2File(String defaultFilename, File is, Map<String, Object> parameters, JRDataSource conn) { try { JasperReport jasperReport = (JasperReport) JRLoader.loadObject(is); JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, conn); exportPdfToFile(jasperPrint, defaultFilename); } catch (Exception e) { e.printStackTrace(); } } // 1.1 导出Pdf private static void exportPdfToFile(JasperPrint jasperPrint, String defaultFilename) throws IOException, JRException { FileOutputStream ouputStream = new FileOutputStream(new File(defaultFilename)); JasperExportManager.exportReportToPdfStream(jasperPrint, ouputStream); ouputStream.flush(); ouputStream.close(); } }
package utils; import java.io.File; import java.io.IOException; import java.util.List; import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.multipdf.PDFMergerUtility; /** * @name: PdfMergeUtil * @Description: pdf合并拼接工具类 * @author wanglong * @date 2020年2月25日下午4:33:13 * @version 1.0 */ public class PdfMergeUtil { /** * @Description: pdf合并拼接 * @param files 文件列表 * @param targetPath 合并到 * @throws IOException */ public static File mulFile2One(List<File> files,String targetPath) throws IOException{ // pdf合并工具类 PDFMergerUtility mergePdf = new PDFMergerUtility(); for (File f : files) { if(f.exists() && f.isFile()){ // 循环添加要合并的pdf mergePdf.addSource(f); } } // 设置合并生成pdf文件名称 mergePdf.setDestinationFileName(targetPath); // 合并pdf mergePdf.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly()); return new File(targetPath); } }
package utils; import javax.servlet.http.HttpServletResponse; import java.io.*; /** * 导出PDF */ public class PdfUtil { // 下载 public static void downloadFile(HttpServletResponse response, String filePath, String fileName) { if (fileName != null) { response.reset(); response.setContentType("application/pdf"); // 设置文件路径 File file = new File(filePath); if (file.exists()) { byte[] buffer = new byte[1024]; FileInputStream fis = null; BufferedInputStream bis = null; try { fis = new FileInputStream(file); bis = new BufferedInputStream(fis); OutputStream os = response.getOutputStream(); int i = bis.read(buffer); while (i != -1) { os.write(buffer, 0, i); i = bis.read(buffer); } System.out.println("download success!"); } catch (Exception e) { e.printStackTrace(); } finally { if (bis != null) { try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } file.delete(); } } } }
以上为个人经验,希望能给大家一个参考,也希望大家多多支持自学编程网。
- 本文固定链接: https://zxbcw.cn/post/221474/
- 转载请注明:必须在正文中标注并保留原文链接
- QQ群: PHP高手阵营官方总群(344148542)
- QQ群: Yii2.0开发(304864863)