diff --git a/config.py b/config.py new file mode 100644 index 0000000..21c6776 --- /dev/null +++ b/config.py @@ -0,0 +1,62 @@ +# 作者 +AUTHOR = "snail" +# 微信公众号 +WECHAT = "ot_bus" +# 子项目默认项目包名 +BASE_PACKAGE = "com.xr.api" +# 默认模块名称,控制controller的URL的前缀 +DEFAULT_PREFIX_URL = "models" +# 项目主目录 +# health-ai 主项目; +MAIN_MODULE = "health-ai" +#health-user-api 模块(子项目) +MODULE_NAME = "health-user-api" +# 项目根目录 +BASE_DIR = "./" +# 输出目录 +# 是否生成REST风格 +REST_CONTROLLER_STYLE = True +# 默认的GROUP_ID +GROUP_ID = "com.xr.health" +OUTPUT_DIR = f"./{MAIN_MODULE}/{MODULE_NAME}/src/main" + +DB = { + "host": "192.168.1.80", + "user": "root", + "password": "Khq#P9hZ4L@EwCZw", + "database": "health_ai_b", + "port": 3728 +} + +APPLICATION = { + "name": MODULE_NAME, + "version": "1.0.0", + "author": AUTHOR, + "package": {"base": BASE_PACKAGE}, + "redis": { + "host": "192.168.1.87", + "port": 6379, + "password": "redis_Hkhtz7", + "database": 4 + }, + "minio": { + "host": "192.168.1.88", + "port": "9001", + "accessKey": "BHB4X5zgZXuzQE6ku31e", + "secretKey": "NZgHX5LhtAVD1KdaucL0EVWy6IQk4owdZLA2A7wi", + "bucketName": "health-bucket", + "downloadDir": "/data/excel" + }, + "xxlJob":{ + "addresses": "http://192.168.1.88:8800/xxl-job-admin", + "accessToken": "xxljobTokenQ", + "address": "192.168.1.88:9999", + "ip": "192.168.1.88", + "port": "9998", + }, + "eureka":{ + "host": "92.168.1.87", + "port": "8761" + }, + "db": DB +} diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..a4b3d9c --- /dev/null +++ b/config.yml @@ -0,0 +1,86 @@ +# =============================== +# 基础项目信息 +# =============================== +mainModule: test-ai +moduleName: test-user-api +groupId: com.test.ai +author: snail +wechat: ot_bus +baseDir: "./" +outputDir: /${mainModule}/${moduleName}/src/main +# 是否启用 Lombok +entityLombokModel: true + +# 是否生成 REST 风格 Controller +restControllerStyle: true + +# =============================== +# 包路径配置 +# =============================== +package: + Models: models + Base: com.test.api + Common: ${package.Base}.${package.Models}.common + Entity: ${package.Base}.${package.Models}.entity + Service: ${package.Base}.${package.Models}.service + ServiceImpl: ${package.Base}.${package.Models}.service.impl + Controller: ${package.Base}.${package.Models}.controller + Mapper: ${package.Base}.${package.Models}.mapper + +# =============================== +# 数据库配置 +# =============================== +db: + host: 192.168.1.80 + port: 3728 + user: root + password: Khq#P9hZ4L@EwCZw + database: health_ai_b + +# =============================== +# 应用级配置 +# =============================== +application: + name: ${moduleName} + version: 1.0.0 + author: snail + + package: + base: ${package.Base} + + # ---------- Redis ---------- + redis: + host: 192.168.1.1 + port: 6379 + password: redis_Hkhtz7 + database: 4 + + # ---------- MinIO ---------- + minio: + host: 192.168.1.1 + port: 9001 + accessKey: BHB4X5zgZXuzQE6ku31e + secretKey: NZgHX5LhtAVD1KdaucL0EVWy6IQk4owdZLA2A7wi + bucketName: health-bucket + downloadDir: /data/excel + + # ---------- XXL-JOB ---------- + xxlJob: + addresses: http://192.168.1.88:8800/xxl-job-admin + accessToken: xxljobTokenQ + address: 192.168.1.1:9999 + ip: 192.168.1.1 + port: 9998 + + # ---------- Eureka ---------- + eureka: + host: 192.168.1.1 + port: 8761 + + # ---------- DB(应用内复用) ---------- + db: + host: 192.168.1.80 + port: 3728 + user: root + password: Khq#P9hZ4L@EwCZw + database: health_ai_b \ No newline at end of file diff --git a/db.py b/db.py new file mode 100644 index 0000000..ede5b27 --- /dev/null +++ b/db.py @@ -0,0 +1,41 @@ +import pymysql +from config import DB + +def get_conn(): + return pymysql.connect( + **DB, + cursorclass=pymysql.cursors.DictCursor + ) + +def get_table(table_name): + sql = """ + SELECT table_name, table_comment + FROM information_schema.tables + WHERE table_schema=%s AND table_name=%s + """ + with get_conn() as conn: + with conn.cursor() as cur: + cur.execute(sql, (DB["database"], table_name)) + return cur.fetchone() + +def get_columns(table_name): + sql = """ + SELECT column_name, data_type, column_comment + FROM information_schema.columns + WHERE table_schema=%s AND table_name=%s + ORDER BY ordinal_position + """ + with get_conn() as conn: + with conn.cursor() as cur: + cur.execute(sql, (DB["database"], table_name)) + return cur.fetchall() + +def mysql_to_java(mysql_type): + mapping = { + "bigint": "Long", + "int": "Integer", + "varchar": "String", + "datetime": "LocalDateTime", + "decimal": "BigDecimal" + } + return mapping.get(mysql_type, "String") diff --git a/generator.py b/generator.py new file mode 100644 index 0000000..81f9acd --- /dev/null +++ b/generator.py @@ -0,0 +1,312 @@ +import logging +import os +from datetime import datetime +from jinja2 import Environment, FileSystemLoader +# from config import * +from db import get_table, get_columns +from utils import * +import argparse +import yaml + +env = Environment(loader=FileSystemLoader("templates")) + +def build_fields(table_name): + columns = get_columns(table_name) + fields = [] + for c in columns: + fields.append({ + "java_name": to_camel(c["column_name"]), + "tab_name": c["column_name"], + "tab_type": c["data_type"], + "java_get_name": to_m_camel(c["column_name"]), + "java_type": mysql_to_java(c["data_type"]), + "comment": c["column_comment"] + }) + return fields + +def render(template_name, out_path, context, overwrite=False): + """ + :param template_name: 模板文件名 + :param out_path: 输出文件路径 + :param context: 渲染上下文 + :param overwrite: 是否覆盖已存在文件,默认 False + """ + # 文件存在且不允许覆盖 → 直接跳过 + if os.path.exists(out_path) and not overwrite: + logging.info("Skip exists file: %s", out_path) + return + + tpl = env.get_template(template_name) + content = tpl.render(**context) + + os.makedirs(os.path.dirname(out_path), exist_ok=True) + with open(out_path, "w", encoding="utf-8") as f: + f.write(content) + + logging.info("Generated file: %s", out_path) + +def generate(table_names: list[str], model_names: list[str], conf_name: str, over_write: bool): + + # context = { + # "mainModule": MAIN_MODULE, + # "moduleName": MODULE_NAME, + # "groupId": GROUP_ID, + # "author": AUTHOR, + # "wechat": WECHAT, + # "date": datetime.now().strftime("%Y-%m-%d"), + # "entityLombokModel": True, + # "package": { + # "Base": BASE_PACKAGE, + # "Common": f"{BASE_PACKAGE}.{DEFAULT_PREFIX_URL}.common", + # "Entity": f"{BASE_PACKAGE}.{DEFAULT_PREFIX_URL}.entity", + # "Service": f"{BASE_PACKAGE}.{DEFAULT_PREFIX_URL}.service", + # "Controller": f"{BASE_PACKAGE}.{DEFAULT_PREFIX_URL}.controller", + # "ServiceImpl": f"{BASE_PACKAGE}.{DEFAULT_PREFIX_URL}.service.impl", + # "Mapper": f"{BASE_PACKAGE}.{DEFAULT_PREFIX_URL}.mapper" + # }, + # "db": DB, + # "application": APPLICATION, + # "restControllerStyle": REST_CONTROLLER_STYLE + # } + + with open(conf_name, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) + cfg = resolve_config(cfg) + + context = { + "mainModule": cfg["mainModule"], + "moduleName": cfg["moduleName"], + "groupId": cfg["groupId"], + "author": cfg["author"], + "wechat": cfg["wechat"], + "date": datetime.now().strftime("%Y-%m-%d"), + "entityLombokModel": cfg["entityLombokModel"], + "package": cfg["package"], + "db": cfg["db"], + "application": cfg["application"], + "restControllerStyle": cfg["restControllerStyle"] + } + # MAIN_BASE_PACKAGE_DIR = f"{OUTPUT_DIR}/java/{to_path(BASE_PACKAGE)}" + # MAIN_OUTPUT_DIR = f"{MAIN_BASE_PACKAGE_DIR}/{DEFAULT_PREFIX_URL}" + + BASE_DIR = cfg["baseDir"] + BASE_PACKAGE = cfg["package"]["Base"] + OUTPUT_DIR = cfg["outputDir"] + MAIN_MODULE = cfg["mainModule"] + MODULE_NAME = cfg["moduleName"] + MAIN_BASE_PACKAGE_DIR = f"{cfg['outputDir']}/java/{to_path(BASE_PACKAGE)}" + MAIN_OUTPUT_DIR = f"{MAIN_BASE_PACKAGE_DIR}/{cfg['package']['Models']}" + + + # ========= 按表循环 ========= + for table_name in table_names: + table = get_table(table_name) + entity = to_class(table_name) + + context = dict(context) + context.update({ + "fields": build_fields(table_name), + "table": { + "entity": entity, + "lowerEntity": lower_first(entity), + "name": table_name, + "comment": table["table_comment"] + } + }) + + # ========= 需要循环生成的模板 ========= + render( + "entity.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/entity/{entity}.java", + context, + over_write + ) + + render( + "controller.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/controller/{entity}Controller.java", + context, + over_write + ) + + render( + "service.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/service/{entity}Service.java", + context, + over_write + ) + + render( + "serviceImpl.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/service/impl/{entity}MPJBaseServiceImpl.java", + context, + over_write + ) + + render( + "mapper.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/mapper/{entity}Mapper.java", + context, + over_write + ) + + render( + "mapper.xml.j2", + f"{MAIN_MODULE}/{MODULE_NAME}/src/main/resources/mappers/{entity}Mapper.xml", + context, + over_write + ) + + # ========= 生成固定模板 ========= + # BaseEntity + render( + "baseEntity.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/entity/BaseEntity.java", + context + ) + + # common MybatisPlusConfig + render( + "mybatisPlusConfig.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/config/MybatisPlusConfig.java", + context + ) + + # common MybatisPlusConfig + render( + "webLogAspect.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/config/WebLogAspect.java", + context + ) + + # common 基础输出result + render( + "result.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/vo/Result.java", + context + ) + + #Util 公共功能 + for file in ["Md5HashUtil.java.j2","FilesUtil.java.j2"]: + render( + file, + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/unit/{file.replace('.j2', '')}", + context + ) + + # application 启动的方法 + render( + "application.java.j2", + f"{BASE_DIR}{MAIN_BASE_PACKAGE_DIR}/Application.java", + context + ) + + # test 测试类 + render( + "applicationTests.java.j2", + f"{BASE_DIR}{OUTPUT_DIR}/test/{to_path(BASE_PACKAGE)}/ApplicationTests.java", + context + ) + + # 主pom文件 + render( + "main.pom.xml.j2", + f"{BASE_DIR}{MAIN_MODULE}/pom.xml", + context + ) + + # 子项目pom文件 + render( + "project.pom.xml.j2", + f"{BASE_DIR}{MAIN_MODULE}/{MODULE_NAME}/pom.xml", + context + ) + + #项目的yml配置文件 resources 生成环境配置为了最低限度能将项目跑起来 + render( + "application.yml.j2", + f"{MAIN_MODULE}/{MODULE_NAME}/src/main/resources/application.yml", + context + ) + + #项目开发环境的yml配置文件 resources yml 只生成dev环境配置为了最低限度能将项目跑起来 + render( + "application-dev.yml.j2", + f"{MAIN_MODULE}/{MODULE_NAME}/src/main/resources/application-dev.yml", + context + ) + + #项目开发环境的yml配置文件 resources yml 只生成dev环境配置为了最低限度能将项目跑起来 + render( + "logback.xml.j2", + f"{MAIN_MODULE}/{MODULE_NAME}/src/main/resources/logback.xml", + context + ) + + # ========= 功能模块 ========= + for model_name in model_names: + match model_name: + case "swagger": + # common Swagger2 + render( + "swagger2.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/config/Swagger2.java", + context + ) + case "saToken": + # common GlobalException soToken 报错自定义 + render( + "globalException.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/config/GlobalException.java", + context + ) + render( + "saTokenConfigure.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/config/SaTokenConfig.java", + context + ) + case "minio": + #MinioConfig + render( + "MinioConfig.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/config/MinioConfig.java", + context + ) + render( + "MinioUpController.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/controller/MinioUpController.java", + context + ) + render( + "MinioUpComponent.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/unit/MinioUpComponent.java", + context + ) + case "xxlJob": + # common XxlJobConfig + render( + "xxlJobConfig.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/config/XxlJobConfig.java", + context + ) + # common xxjob的测试类 + render( + "testJob.java.j2", + f"{BASE_DIR}{MAIN_OUTPUT_DIR}/common/job/TestJob.java", + context + ) + +if __name__ == "__main__": + args = parse_args() + + tables = [t.strip() for t in args.tab.split(",") if t.strip()] + models = [m.strip() for m in args.model.split(",") if m.strip()] + conf = args.conf + re = args.re + + generate( + table_names=tables, + model_names=models, + conf_name=conf, + over_write=re + ) \ No newline at end of file diff --git a/templates/FilesUtil.java.j2 b/templates/FilesUtil.java.j2 new file mode 100644 index 0000000..e8b27f4 --- /dev/null +++ b/templates/FilesUtil.java.j2 @@ -0,0 +1,142 @@ +package {{ package.Common }}.unit; + +import org.apache.commons.io.FileUtils; +import org.springframework.web.multipart.MultipartFile; +import sun.misc.BASE64Encoder; + +import java.io.*; +import java.util.Base64; + +public class FilesUtil { + + /** + * 根据文件路径获取文件字节流 + * @return + * filePath 文件路径 + * @throws IOException + */ + public static byte[] toByteArray(String filePath) throws IOException { + File f = new File(filePath); + if (!f.exists()) { + throw new FileNotFoundException("文件不存在"); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream((int) f.length()); + BufferedInputStream in = null; + try { + in = new BufferedInputStream(new FileInputStream(f)); + int buf_size = 1024; + byte[] buffer = new byte[buf_size]; + int len = 0; + while (-1 != (len = in.read(buffer, 0, buf_size))) { + bos.write(buffer, 0, len); + } + return bos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + throw e; + } finally { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + bos.close(); + } + } + public static File multipartFileToFile(MultipartFile file) throws Exception { + File toFile = null; + if (file.equals("") || file.getSize() <= 0) { + file = null; + } else { + InputStream ins = null; + ins = file.getInputStream(); + toFile = new File(file.getOriginalFilename()); + inputStreamToFile(ins, toFile); + ins.close(); + } + return toFile; + + } + + + + private static void inputStreamToFile(InputStream ins, File file) { + try { + OutputStream os = new FileOutputStream(file); + int bytesRead = 0; + byte[] buffer = new byte[8192]; + while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) { + os.write(buffer, 0, bytesRead); + } + os.close(); + ins.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static String TransformPhotoToBase64Data(String path){ + Base64.Encoder encoder= Base64.getEncoder(); //获取Base64编码器 + byte [] ImgContainer = null ; //数据集缓存器 + FileInputStream fileInputStream = null; //文件输入流 + try { + System.out.println(path); + File file=new File(path); + fileInputStream = new FileInputStream(file); //到指定路径寻找文件 + ImgContainer = new byte[fileInputStream.available()]; //设置图片字节数据缓冲区大小 + fileInputStream.read(ImgContainer); //将数据流中的图片数据读进缓冲区 + String Base64ImgData =encoder.encodeToString(ImgContainer); //将图片编码转换成Base64格式的数据集 + fileInputStream.close(); //关闭数据流 + return Base64ImgData; //将缓冲区数据转换成字符数据返回 + } catch (FileNotFoundException e) { + return "找不到指定文件!"; + } catch (IOException e) { + e.printStackTrace(); + } + return "null"; + } + + public static String getBase64String(MultipartFile multiPartFile) throws IOException { + String baseStr = null; + + //把MultipartFile转化为File + File file = new File(multiPartFile.getOriginalFilename()); + FileUtils.copyInputStreamToFile(multiPartFile.getInputStream(), file); + + try {//file转base64 + FileInputStream inputStream = new FileInputStream(file); + byte[] buffer = new byte[(int) file.length()]; + inputStream.read(buffer); + inputStream.close(); + baseStr = new BASE64Encoder().encode(buffer); + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + //删除临时文件 + if (file.exists()) { + file.delete(); + } + baseStr = baseStr.replaceAll("\r\n", ""); + return baseStr; + } + + /** + * 返回文件扩展名,带。 + * @param file + * @return + */ + public static String getFileExtension(MultipartFile file) { + String fileName = file.getOriginalFilename(); + int dotIndex = fileName.lastIndexOf("."); + if (dotIndex == -1) { + return ""; + } else { + return "."+fileName.substring(dotIndex + 1); + } + } + +} diff --git a/templates/Md5HashUtil.java.j2 b/templates/Md5HashUtil.java.j2 new file mode 100644 index 0000000..3c98768 --- /dev/null +++ b/templates/Md5HashUtil.java.j2 @@ -0,0 +1,21 @@ +package {{ package.Common }}.unit; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class Md5HashUtil { + + public static String getMD5Hash(byte[] input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(input); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Could not find MD5 algorithm", e); + } + } +} diff --git a/templates/MinioConfig.java.j2 b/templates/MinioConfig.java.j2 new file mode 100644 index 0000000..3129ee5 --- /dev/null +++ b/templates/MinioConfig.java.j2 @@ -0,0 +1,84 @@ +package {{ package.Common }}.config; +import {{ package.Common }}.unit.MinioUpComponent; + + +import io.minio.MinioClient; +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import java.io.Serializable; + +/** + * @Description: minio配置类 + * @Author: {{author}} + * @Date: {{date}} + * @wechat: {{ wechat }} + */ + +@Component +@Data +@ConditionalOnClass(MinioUpComponent.class) +public class MinioConfig implements Serializable { + + + /** + * API调用地址ip + */ + @Value("${minio.config.ip}") + private String ip; + + /** + * API调用地址端口 + */ + @Value("${minio.config.port}") + private String port; + + /** + * 连接账号 + */ + @Value("${minio.config.accessKey}") + private String accessKey; + + /** + * 连接秘钥 + */ + @Value("${minio.config.secretKey}") + private String secretKey; + + /** + * minio存储桶的名称 + */ + @Value("${minio.config.bucketName}") + private String bucketName; + + /** + * 文件下载到本地的路径 + */ + @Value("${minio.config.downloadDir}") + private String downloadDir; + + /** + * #如果是true,则用的是https而不是http,默认值是true + */ + @Value("${minio.config.secure}") + private Boolean secure; + + @Value("${minio.config.readPath}") + private String readPath; + + @Value("${minio.config.endpoint}") + private String endpoint; + + @Bean + public MinioClient buildClient() { + //1.创建minio链接客户端 + return MinioClient + .builder() + .credentials(accessKey, secretKey) + .endpoint(endpoint) + .build(); + } + +} diff --git a/templates/MinioUpComponent.java.j2 b/templates/MinioUpComponent.java.j2 new file mode 100644 index 0000000..0bee741 --- /dev/null +++ b/templates/MinioUpComponent.java.j2 @@ -0,0 +1,191 @@ +package {{ package.Common }}.unit; +import {{ package.Common }}.config.MinioConfig; + +import io.minio.*; +import io.minio.http.Method; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.TimeUnit; + + +/**** + * @Description: 上传 文件组件 + * @Author: {{author}} + * @Date: {{date}} + * @Wechat: {{ wechat }} + */ +@Slf4j +@Component +public class MinioUpComponent { + + private final static String separator = "/"; + @Autowired + private MinioConfig minioConfig; + @Autowired + private MinioClient minioClient; + + /** + * @param dirPath + * @param filename yyyy/mm/dd/file.jpg + * @return + */ + public static String builderFilePath(String dirPath, String filename) { + StringBuilder stringBuilder = new StringBuilder(50); + if (!StringUtils.isEmpty(dirPath)) { + stringBuilder.append(dirPath).append(separator); + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); + String todayStr = sdf.format(new Date()); + stringBuilder.append(todayStr).append(separator); + stringBuilder.append(filename); + return stringBuilder.toString(); + } + + /** + * 上传图片文件 + * + * @param prefix 文件前缀 + * @param filename 文件名 + * @param inputStream 文件流 + * @return 文件全路径 + */ + public String uploadImg(String prefix, String filename, InputStream inputStream) { + String filePath = builderFilePath(prefix, filename); + try { + PutObjectArgs putObjectArgs = PutObjectArgs.builder() + .object(filePath) + .contentType("image/jpg") + .bucket(minioConfig.getBucketName()).stream(inputStream, inputStream.available(), -1) + .build(); + minioClient.putObject(putObjectArgs); + StringBuilder urlPath = new StringBuilder(minioConfig.getReadPath()); + urlPath.append(separator + minioConfig.getBucketName()); + urlPath.append(separator); + urlPath.append(filePath); + return urlPath.toString(); + } catch (Exception ex) { + log.error("minio put file error.", ex); + throw new RuntimeException("上传文件失败"); + } + } + + /** + * 上传html文件 + * + * @param prefix 文件前缀 + * @param filename 文件名 + * @param inputStream 文件流 + * @return 文件全路径 + */ + public String uploadHtml(String prefix, String filename, InputStream inputStream) { + String filePath = builderFilePath(prefix, filename); + try { + PutObjectArgs putObjectArgs = PutObjectArgs.builder() + .object(filePath) + .contentType("text/html") + .bucket(minioConfig.getBucketName()) + .stream(inputStream, inputStream.available(), -1) + .build(); + minioClient.putObject(putObjectArgs); + StringBuilder urlPath = new StringBuilder(minioConfig.getReadPath()); + urlPath.append(separator + minioConfig.getBucketName()); + urlPath.append(separator); + urlPath.append(filePath); + return urlPath.toString(); + } catch (Exception ex) { + log.error("minio put file error.", ex); + ex.printStackTrace(); + throw new RuntimeException("上传文件失败"); + } + } + + /** + * 防盗链接有效期按分钟 + * + * @param filePath 文件前缀 + * @param expiry 超时分钟 + * @return 文件全路径 + */ + public String getPresignObjectUrl(String filePath, Integer expiry) { + try { + filePath = filePath.replace("http://health.reglory.com.cn:9001/health-bucket/", ""); + GetPresignedObjectUrlArgs putObjectArgs = GetPresignedObjectUrlArgs.builder() + .object(filePath) + .bucket(minioConfig.getBucketName()) + .method(Method.GET) +// .region("health.reglory.com.cn:9001") + .expiry(expiry, TimeUnit.MINUTES) + .build(); + return minioClient.getPresignedObjectUrl(putObjectArgs); + } catch (Exception ex) { + log.error("minio put file error.", ex); + ex.printStackTrace(); + throw new RuntimeException("上传文件失败"); + } + } + + /** + * 删除文件 + * + * @param pathUrl 文件全路径 + */ + public void delete(String pathUrl) { + String key = pathUrl.replace(minioConfig.getEndpoint() + "/", ""); + int index = key.indexOf(separator); + String bucket = key.substring(0, index); + String filePath = key.substring(index + 1); + // 删除Objects + RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(bucket).object(filePath).build(); + try { + minioClient.removeObject(removeObjectArgs); + } catch (Exception e) { + log.error("minio remove file error. pathUrl:{}", pathUrl); + e.printStackTrace(); + } + } + + + /** + * 下载文件 + * + * @param pathUrl 文件全路径 + * @return 文件流 + */ + public byte[] downLoadFile(String pathUrl) { + String key = pathUrl.replace(minioConfig.getEndpoint() + "/", ""); + int index = key.indexOf(separator); + //String bucket = key.substring(0, index); + String filePath = key.substring(index + 1); + InputStream inputStream = null; + try { + inputStream = minioClient.getObject( + GetObjectArgs.builder().bucket(minioConfig.getBucketName()).object(filePath).build() + ); + } catch (Exception e) { + log.error("minio down file error. pathUrl:{}", pathUrl); + e.printStackTrace(); + } + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + byte[] buff = new byte[100]; + int rc = 0; + while (true) { + try { + if (!((rc = inputStream.read(buff, 0, 100)) > 0)) break; + } catch (IOException e) { + e.printStackTrace(); + } + byteArrayOutputStream.write(buff, 0, rc); + } + return byteArrayOutputStream.toByteArray(); + } +} + diff --git a/templates/MinioUpController.java.j2 b/templates/MinioUpController.java.j2 new file mode 100644 index 0000000..239ef04 --- /dev/null +++ b/templates/MinioUpController.java.j2 @@ -0,0 +1,53 @@ +package {{ package.Controller }}; +import {{ package.Common }}.unit.FilesUtil; +import {{ package.Common }}.unit.Md5HashUtil; +import {{ package.Common }}.unit.MinioUpComponent; +import {{ package.Common }}.vo.Result; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +/** + *
+ * 上传控制器 + *
+ * + * @author tiger + * @since 2024-06-04 + */ +@Api(tags = "文件上传") +@RestController +@CrossOrigin(origins = "*") +@RequestMapping("/models/file") +public class MinioUpController { + + @Autowired + private MinioUpComponent minioUpComponent; + + @ApiOperation(value = "图片上传") + @CrossOrigin(origins = "*") + @PostMapping(value = "/upImg") + public Result> up(@RequestParam("file") MultipartFile file) { + if (file.isEmpty()) { + return Result.error("请选择一个文件上传"); + } + // 文件上传路径 可以为空 默认 bucketName: "health-bucket" 桶的名字为根目录 + // String path = "/tmp/user"; + String path = ""; + String url = ""; + try { + url = minioUpComponent.uploadImg(path,Md5HashUtil.getMD5Hash(file.getBytes()) + + FilesUtil.getFileExtension(file), file.getInputStream()); + } catch (IOException e) { + return Result.error("上传失败"); + } + return Result.OK(url); + } + + +} diff --git a/templates/application-dev.yml.j2 b/templates/application-dev.yml.j2 new file mode 100644 index 0000000..fd7e5ca --- /dev/null +++ b/templates/application-dev.yml.j2 @@ -0,0 +1,137 @@ +server: + port: 8060 + servlet: + context-path: /api + +spring: +############## datasource 相关 + datasource: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://{{ application.db.host }}:{{ application.db.port }}/{{ application.db.database }}?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false + username: {{ application.db.user }} + password: {{ application.db.password }} + #Hikari连接池配置 + hikari: + #池中维护的最小空闲连接数 + minimum-idle: 5 + #池中最大连接数,包括闲置和使用中的连接 + maximum-pool-size: 15 + #自动提交从池中返回的连接 + auto-commit: true + #连接允许在池中闲置的最长时间 + idle-timeout: 30000 + #连接池的用户定义名称,主要出现在日志记录和JMX管理控制台中以识别池和池配置 + pool-name: DatebookHikariCP + #池中连接最长生命周期 + max-lifetime: 18000000 + #等待来自池的连接的最大毫秒数 + connection-timeout: 30000 + #验证该连接是否是有效的查询语句 + connection-test-query: select 1 from dual + cloud: + inetutils: + timeout-seconds: 10 + config: + enabled: false +############## redis 相关 + redis: + host: {{ application.redis.host }} + port: {{ application.redis.port }} + password: {{ application.redis.password }} + timeout: 10000 + jedis: + pool: + max-active: 1000 + max-wait: -1ms + max-idle: 10 + min-idle: 5 + database: {{application.redis.database}} +############## eureka 相关 +eureka: + instance: + instance-id: ${spring.cloud.client.ip-address}:${server.port} + prefer-ip-address: true + client: + healthcheck: + enabled: true + service-url: + defaultZone: http://{{ application.eureka.host }}:{{ application.eureka.port }}/eureka + +############## mybatis-plus配置 +mybatis-plus: + # 启动检查MyBatis配置文件 + check-config-location: false + # MyBatis配置文件位置 + config-location: + # MyBaits别名包扫描路径 + type-aliases-package: com.qiangesoft.mybatisplusjoin.entity + # Mapper所对应的XML文件位置 默认【classpath*:/mapper/**/*.xml】 + mapper-locations: classpath*:/mapper/*Mapper.xml + # TypeHandler扫描路径 + type-handlers-package: + configuration: + # 日志打印 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 是否开启自动驼峰命名规则 + map-underscore-to-camel-case: true + # 开启Mybatis二级缓存,默认为true + cache-enabled: true + global-config: + # 控制台mybatis-plus的logo + banner: true + db-config: + # 全局默认主键类型 + id-type: auto + # 逻辑删除配置 + logic-delete-field: deleted + logic-delete-value: 1 + logic-not-delete-value: 0 + +############## Sa-Token 配置 (文档: https://sa-token.cc) ############## +sa-token: + # token 名称(同时也是 cookie 名称) + token-name: Token + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: true + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # 是否输出操作日志 + is-log: true + +############## swagger +swagger: + show: true + +############## minio +minio: + config: + ip: {{ application.minio.host }} #ip地址 + port: {{ application.minio.port }} # 端口号 + accessKey: {{ application.minio.accessKey }} # 账号 + secretKey: {{ application.minio.secretKey }} # 密码 + secure: false #如果是true,则用的是https而不是http,默认值是true + bucketName: "{{ application.minio.bucketName }}" # 桶的名字 + downloadDir: "{{ application.minio.downloadDir }}" #保存到本地的路径 + readPath: "http://{{ application.minio.host }}:{{ application.minio.port }}" #保存到本地的路径 + endpoint: "http://{{ application.minio.host }}:{{ application.minio.port }}" #保存到本地的路径 + +############## xxl-job +xxl: + job: + admin: + addresses: {{ application.xxlJob.addresses }} # 调度中心部署根地址 + accessToken: "{{ application.xxlJob.accessToken }}" # 执行器通讯TOKEN,非空时启用 + executor: + appname: xxl-{{ application.name }} # 执行器AppName + address: {{ application.xxlJob.address }} # 执行器注册地址,为空时使用内嵌服务IP:PORT + ip: {{ application.xxlJob.ip }} # 执行器IP,默认为空自动获取 + port: {{ application.xxlJob.port }} # 执行器端口号 + logpath: /data/applogs/xxl-job/jobhandler # 执行器运行日志文件存储磁盘路径 + logretentiondays: 30 # 执行器日志文件保存天数 \ No newline at end of file diff --git a/templates/application.java.j2 b/templates/application.java.j2 new file mode 100644 index 0000000..02116bc --- /dev/null +++ b/templates/application.java.j2 @@ -0,0 +1,48 @@ +package {{ package.Base }}; + +//-- 固定引入 --// +import com.fasterxml.jackson.core.JsonProcessingException; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +//-- 固定引入 --// + +/** + * @Description: 启动类 + * @Author: {{author}} + * @Date: {{date}} + * @Wechat: {{ wechat }} + */ +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +@EnableCaching +public class Application { + + public static void main(String[] args) throws JsonProcessingException { + SpringApplication.run(Application.class, args); + } + + + + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); // 允许cookies跨域 + config.addAllowedOrigin("*");// 允许向该服务器提交请求的URI,*表示全部允许。。这里尽量限制来源域,比如http://xxxx:8080 ,以降低安全风险。。 + config.addAllowedHeader("*");// 允许访问的头信息,*表示全部 + config.setMaxAge(18000L);// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了 + config.addAllowedMethod("*");// 允许提交请求的方法,*表示全部允许,也可以单独设置GET、PUT等 + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + + +} diff --git a/templates/application.yml.j2 b/templates/application.yml.j2 new file mode 100644 index 0000000..5e65a7f --- /dev/null +++ b/templates/application.yml.j2 @@ -0,0 +1,26 @@ +spring: + profiles: + active: dev #开发环境 + #active: test #测试环境 + #active: pro #生产环境 + application: + name: {{ application.name }} + devtools: + restart: + log-condition-evaluation-delta: false + security: + user: + name: admin + password: admin + servlet: + multipart: + max-file-size: 30MB + max-request-size: 30MB +logging: + level: + root: info + {{ application.package.base }}: debug + org: + springframework: + boot: + autoconfigure: error diff --git a/templates/applicationTests.java.j2 b/templates/applicationTests.java.j2 new file mode 100644 index 0000000..097a810 --- /dev/null +++ b/templates/applicationTests.java.j2 @@ -0,0 +1,23 @@ +package {{ package.Base }}; + + /*** + * @description {{ projectName }} + * @version 1.0.0 + * @Author: {{author}} + * @Date: {{date}} + * @Wechat: {{ wechat }} + */ + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import java.io.IOException; + +@SpringBootTest +class ApplicationTests { + + @Test + void contextLoads() throws IOException { + System.out.println("test start"); + } + +} diff --git a/templates/baseEntity.java.j2 b/templates/baseEntity.java.j2 new file mode 100644 index 0000000..ef85f60 --- /dev/null +++ b/templates/baseEntity.java.j2 @@ -0,0 +1,86 @@ +package {{ package.Entity }}; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.Date; + +/** + * 基础Entity类 + * @Author: {{author}} + * @Date: {{date}} + * @Wechat: {{ wechat }} + */ + +@ApiModel("基础Entity类") +@Setter +@Getter +public class BaseEntity implements Serializable { + + @ApiModelProperty(value = "主键id") + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + @ApiModelProperty(value = "创建者", hidden = true) + protected Integer createdBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @ApiModelProperty(value = "创建日期", hidden = true, example = "yyyy-MM-dd HH:mm:ss") + @TableField(value = "created_at", fill = FieldFill.INSERT) + protected Date createdAt; + + @ApiModelProperty(value = "更新者", hidden = true) + protected Integer updatedBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @ApiModelProperty(value = "更新日期", hidden = true, example = "yyyy-MM-dd HH:mm:ss") + @TableField(value = "updated_at", fill = FieldFill.INSERT, update = "NOW()") + protected Date updatedAt; + + @ApiModelProperty(value = "删除标记", hidden = true) + @TableField(value = "deleted_flag") + @TableLogic(value = "0", delval = "1") + protected Integer deletedFlag; + + @ApiModelProperty(value = "页码", required = false) + @TableField(exist = false) + private Integer pageNum; //页码 + + @ApiModelProperty(value = "每页条数", required = false) + @TableField(exist = false) + private Integer pageSize;//每页条数 + + @ApiModelProperty(value = "排序方式排序[true:正序; false:倒序]", required = false) + @TableField(exist = false) + private Boolean sort; + + @ApiModelProperty(value = "排序字段,参照返回字段", required = false) + @TableField(exist = false) + private String sortName; + + @ApiModelProperty(value = "用户ID") + @TableField(value = "user_id") + private Integer userId; + + @ApiModelProperty(value = "创建者用户名") + @TableField(exist = false) + private String createdByName; + + @ApiModelProperty(value = "更新者用户名") + @TableField(exist = false) + private String updatedByName; + + public BaseEntity() { + if (this.pageSize == null) { + this.setPageSize(10); + } + if (this.pageNum == null) { + this.setPageNum(1); + } + } +} diff --git a/templates/controller.java.j2 b/templates/controller.java.j2 new file mode 100644 index 0000000..2a5c1cf --- /dev/null +++ b/templates/controller.java.j2 @@ -0,0 +1,107 @@ +package {{ package.Controller }}; +import {{ package.Entity }}.{{ table.entity }}; +import {{ package.Service }}.{{ table.entity }}Service; +import {{ package.Common }}.vo.Result; + +//--- import 固定引入 ---// +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +//--- import 固定引入 ---// + +/** + *+ * {{ table.comment }} 前端控制器 + *
+ * @Author: {{author}} + * @Date: {{date}} + * @Wechat: {{ wechat }} + */ +@Api(tags = "{{ table.comment }}") +{% if restControllerStyle %} +@RestController +{% else %} +@Controller +{% endif %} +@RequestMapping("{{ table.name }}") +public class {{ table.entity }}Controller { + + @Resource + private {{ table.entity }}Service {{table.lowerEntity}}Service; + + @ApiOperation(value = "{{ table.comment }}分页列表查询", response = {{ table.entity }}.class) + @PostMapping(value = "/page") + public Result