wangzaijun hai 2 meses
achega
3b20c86533
Modificáronse 100 ficheiros con 6904 adicións e 0 borrados
  1. 33 0
      .gitignore
  2. BIN=BIN
      mo-daq-openai/requirements.txt
  3. BIN=BIN
      mo-daq-openai/web/__pycache__/main.cpython-312.pyc
  4. BIN=BIN
      mo-daq-openai/web/__pycache__/route.cpython-312.pyc
  5. 9 0
      mo-daq-openai/web/main.py
  6. 68 0
      mo-daq-openai/web/route.py
  7. 0 0
      mo-daq-openai/新建文本文档.txt
  8. 2 0
      mo-daq/.gitattributes
  9. 33 0
      mo-daq/.gitignore
  10. 19 0
      mo-daq/.mvn/wrapper/maven-wrapper.properties
  11. 243 0
      mo-daq/db/init.sql
  12. 259 0
      mo-daq/mvnw
  13. 149 0
      mo-daq/mvnw.cmd
  14. 204 0
      mo-daq/pom.xml
  15. 0 0
      mo-daq/readme.md
  16. 13 0
      mo-daq/src/main/java/com/smppw/modaq/MoDaqApplication.java
  17. 30 0
      mo-daq/src/main/java/com/smppw/modaq/application/api/ParseApi.java
  18. 52 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/CustomPDFTextStripper.java
  19. 194 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/CustomTabulaTextStripper.java
  20. 427 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/ReportParseUtils.java
  21. 128 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/AbstractReportParser.java
  22. 33 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/ReportParser.java
  23. 63 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/ReportParserConstant.java
  24. 32 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/ReportParserFactory.java
  25. 210 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/pdf/AbstractPDReportParser.java
  26. 143 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/pdf/PDAnnuallyReportParser.java
  27. 92 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/pdf/PDLetterReportParser.java
  28. 89 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/pdf/PDMonthlyReportParser.java
  29. 266 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/pdf/PDQuarterlyReportParser.java
  30. 52 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/AbstractReportWriter.java
  31. 14 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/AnnuallyReportWriter.java
  32. 35 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/LetterReportWriter.java
  33. 25 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/MonthlyReportWriter.java
  34. 62 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/QuarterlyReportWriter.java
  35. 13 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/ReportWriter.java
  36. 23 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/ReportWriterConstant.java
  37. 26 0
      mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/ReportWriterFactory.java
  38. 40 0
      mo-daq/src/main/java/com/smppw/modaq/application/service/EmailParseApiService.java
  39. 277 0
      mo-daq/src/main/java/com/smppw/modaq/application/service/EmailParseApiServiceImpl.java
  40. 25 0
      mo-daq/src/main/java/com/smppw/modaq/application/task/ParseSchedulerTask.java
  41. 367 0
      mo-daq/src/main/java/com/smppw/modaq/application/util/EmailUtil.java
  42. 7 0
      mo-daq/src/main/java/com/smppw/modaq/common/conts/Constants.java
  43. 12 0
      mo-daq/src/main/java/com/smppw/modaq/common/conts/DateConst.java
  44. 15 0
      mo-daq/src/main/java/com/smppw/modaq/common/conts/EmailParseStatusConst.java
  45. 25 0
      mo-daq/src/main/java/com/smppw/modaq/common/conts/EmailTypeConst.java
  46. 36 0
      mo-daq/src/main/java/com/smppw/modaq/common/enums/ReportParseStatus.java
  47. 30 0
      mo-daq/src/main/java/com/smppw/modaq/common/enums/ReportParserFileType.java
  48. 21 0
      mo-daq/src/main/java/com/smppw/modaq/common/enums/ReportType.java
  49. 33 0
      mo-daq/src/main/java/com/smppw/modaq/common/enums/ResultCode.java
  50. 7 0
      mo-daq/src/main/java/com/smppw/modaq/common/enums/StatusCode.java
  51. 36 0
      mo-daq/src/main/java/com/smppw/modaq/common/exception/ReportParseException.java
  52. 17 0
      mo-daq/src/main/java/com/smppw/modaq/common/support/DTO.java
  53. 41 0
      mo-daq/src/main/java/com/smppw/modaq/common/support/dos/BaseEntity.java
  54. 36 0
      mo-daq/src/main/java/com/smppw/modaq/common/support/dos/DataEntity.java
  55. 41 0
      mo-daq/src/main/java/com/smppw/modaq/common/support/dos/OnlyIdNameDO.java
  56. 10 0
      mo-daq/src/main/java/com/smppw/modaq/common/support/vo/BaseMultiJoinVO.java
  57. 18 0
      mo-daq/src/main/java/com/smppw/modaq/common/support/vo/BaseVO.java
  58. 28 0
      mo-daq/src/main/java/com/smppw/modaq/common/support/vo/OnlyIdNameVO.java
  59. 71 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/EmailContentInfoDTO.java
  60. 46 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/EmailInfoDTO.java
  61. 17 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/EmailZipFileDTO.java
  62. 38 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/MailboxInfoDTO.java
  63. 50 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/QuartzBean.java
  64. 18 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/AnnuallyReportData.java
  65. 89 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/BaseReportDTO.java
  66. 32 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/BaseReportLevelDTO.java
  67. 30 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/LetterReportData.java
  68. 25 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/MonthlyReportData.java
  69. 28 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ParseResult.java
  70. 41 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/QuarterlyReportData.java
  71. 61 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportAssetAllocationDTO.java
  72. 56 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportBaseInfoDTO.java
  73. 55 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportData.java
  74. 92 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFinancialIndicatorsDTO.java
  75. 65 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFundInfoDTO.java
  76. 293 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFundTransactionDTO.java
  77. 61 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportInvestmentIndustryDTO.java
  78. 53 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportInvestorInfoDTO.java
  79. 72 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportNetReportDTO.java
  80. 34 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportParserParams.java
  81. 72 0
      mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportShareChangeDTO.java
  82. 57 0
      mo-daq/src/main/java/com/smppw/modaq/domain/entity/EmailFieldMappingDO.java
  83. 59 0
      mo-daq/src/main/java/com/smppw/modaq/domain/entity/EmailFileInfoDO.java
  84. 85 0
      mo-daq/src/main/java/com/smppw/modaq/domain/entity/EmailParseInfoDO.java
  85. 93 0
      mo-daq/src/main/java/com/smppw/modaq/domain/entity/MailboxInfoDO.java
  86. 23 0
      mo-daq/src/main/java/com/smppw/modaq/domain/entity/report/BaseReportDO.java
  87. 30 0
      mo-daq/src/main/java/com/smppw/modaq/domain/entity/report/ReportBaseInfoDO.java
  88. 35 0
      mo-daq/src/main/java/com/smppw/modaq/domain/entity/report/ReportFundInfoDO.java
  89. 242 0
      mo-daq/src/main/java/com/smppw/modaq/domain/entity/report/ReportFundTransactionDO.java
  90. 40 0
      mo-daq/src/main/java/com/smppw/modaq/domain/entity/report/ReportInvestorInfoDO.java
  91. 19 0
      mo-daq/src/main/java/com/smppw/modaq/domain/mapper/EmailFieldMappingMapper.java
  92. 38 0
      mo-daq/src/main/java/com/smppw/modaq/domain/mapper/EmailFileInfoMapper.java
  93. 37 0
      mo-daq/src/main/java/com/smppw/modaq/domain/mapper/EmailParseInfoMapper.java
  94. 31 0
      mo-daq/src/main/java/com/smppw/modaq/domain/mapper/MailboxInfoMapper.java
  95. 9 0
      mo-daq/src/main/java/com/smppw/modaq/domain/mapper/ReportBaseInfoMapper.java
  96. 9 0
      mo-daq/src/main/java/com/smppw/modaq/domain/mapper/ReportFundInfoMapper.java
  97. 9 0
      mo-daq/src/main/java/com/smppw/modaq/domain/mapper/ReportFundTransactionMapper.java
  98. 9 0
      mo-daq/src/main/java/com/smppw/modaq/domain/mapper/ReportInvestorInfoMapper.java
  99. 517 0
      mo-daq/src/main/java/com/smppw/modaq/domain/service/EmailParseService.java
  100. 0 0
      mo-daq/src/main/java/com/smppw/modaq/infrastructure/config/DataSourceAutoConfig.java

+ 33 - 0
.gitignore

@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/

BIN=BIN
mo-daq-openai/requirements.txt


BIN=BIN
mo-daq-openai/web/__pycache__/main.cpython-312.pyc


BIN=BIN
mo-daq-openai/web/__pycache__/route.cpython-312.pyc


+ 9 - 0
mo-daq-openai/web/main.py

@@ -0,0 +1,9 @@
+from route import app
+
+
+if __name__ == '__main__':
+    import uvicorn
+
+    print("run", app.title)
+
+    uvicorn.run('main:app', host="0.0.0.0", port=8088, reload=True)

+ 68 - 0
mo-daq-openai/web/route.py

@@ -0,0 +1,68 @@
+import os
+from pathlib import Path
+
+from fastapi import FastAPI, File, UploadFile
+from openai import OpenAI
+
+app = FastAPI()
+
+client = OpenAI(
+    api_key=os.getenv("DASHSCOPE_API_KEY"),  # 如果您没有配置环境变量,请在此处替换您的API-KEY
+    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",  # 填写DashScope服务base_url
+)
+
+
+@app.get("/upload-filepath")
+async def create_upload_file(filepath: str = None,
+                             file_id: str = None,
+                             user_msg: str = '请准确提取文件中的表格内容,要求用字典返回并去掉金额单位、英文和空格'):
+    # 读取文件内容(可选)
+    # contents = await file.read()
+
+    # 这里可以对文件进行进一步处理,比如保存到服务器上
+    # with open(f"./{file.filename}", "wb") as f:
+    #     f.write(contents)
+
+    if file_id is None:
+        file_object = client.files.create(file=Path(filepath), purpose="file-extract")
+        file_id = file_object.id
+
+    # 初始化messages列表
+    completion = client.chat.completions.create(
+        model="qwen-long",
+        messages=[
+            {'role': 'system', 'content': 'You are a helpful assistant.'},
+            {'role': 'system', 'content': f'fileid://{file_id}'},
+            {'role': 'user', 'content': user_msg}
+        ],
+    )
+
+    return {"file_id": file_id, "content": completion.choices[0].message.content}
+
+
+@app.post("/upload-file")
+async def create_upload_file(file: UploadFile = File(...),
+                             file_id: str = None,
+                             user_msg: str = '请准确提取文件中的表格内容,要求用字典返回并去掉金额单位、英文和空格'):
+    if file_id is None:
+        # 读取文件内容(可选)
+        contents = await file.read()
+
+        # 这里可以对文件进行进一步处理,比如保存到服务器上
+        with open(f"./uploads/{file.filename}", "wb") as f:
+            f.write(contents)
+
+        file_object = client.files.create(file=Path(f"./uploads/{file.filename}"), purpose="file-extract")
+        file_id = file_object.id
+
+    # 初始化messages列表
+    completion = client.chat.completions.create(
+        model="qwen-long",
+        messages=[
+            {'role': 'system', 'content': 'You are a helpful assistant.'},
+            {'role': 'system', 'content': f'fileid://{file_id}'},
+            {'role': 'user', 'content': user_msg}
+        ],
+    )
+
+    return {"file_id": file_id, "content": completion.choices[0].message.content}

+ 0 - 0
mo-daq-openai/新建文本文档.txt


+ 2 - 0
mo-daq/.gitattributes

@@ -0,0 +1,2 @@
+/mvnw text eol=lf
+*.cmd text eol=crlf

+ 33 - 0
mo-daq/.gitignore

@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/

+ 19 - 0
mo-daq/.mvn/wrapper/maven-wrapper.properties

@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip

+ 243 - 0
mo-daq/db/init.sql

@@ -0,0 +1,243 @@
+create table rz_market_operate_ppwfund.mo_mailbox_info
+(
+    id          int auto_increment comment '主键Id'
+        primary key,
+    type        int                                null comment '邮箱类型:1-QQ邮箱,2-腾讯企业邮箱,3-网易邮箱,4-新浪邮箱,99-其他',
+    email       varchar(255)                       null comment '邮箱账号',
+    password    varchar(255)                       null comment '邮箱密码',
+    protocol    varchar(255)                       null comment '协议',
+    server      varchar(255)                       null comment '收件服务器',
+    port        varchar(255)                       null comment '端口',
+    cron        varchar(255)                       null comment 'cron表达式',
+    open_status int      default 0                 null comment '是否开启,0-不开启,1-开启',
+    description text                               null comment '备注信息',
+    isvalid     int      default 1                 null comment '记录的有效性;1-有效;0-无效;',
+    creatorid   int                                null comment '创建者Id;第一次创建时与Creator值相同,修改时与修改人值相同',
+    updaterid   int                                null comment '修改者Id;第一次创建时与Creator值相同,修改时与修改人值相同',
+    createtime  datetime default CURRENT_TIMESTAMP null comment '创建时间,默认第一次创建的getdate()时间',
+    updatetime  datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间;第一次创建时与CreatTime值相同,修改时与修改时间相同'
+)
+    comment '邮箱信息表';
+
+create table rz_market_operate_ppwfund.mo_email_field_mapping
+(
+    id         int auto_increment comment '主键Id'
+        primary key,
+    code       varchar(255)                       null comment '字段编码',
+    name       text                               null comment '字段(多个以英文逗号隔开)',
+    type       int                                null comment '3-定期报告,4-交易流水确认函,0-共用的',
+    isvalid    int      default 1                 null comment '记录的有效性;1-有效;0-无效;',
+    creatorid  int                                null comment '创建者Id;第一次创建时与Creator值相同,修改时与修改人值相同',
+    updaterid  int                                null comment '修改者Id;第一次创建时与Creator值相同,修改时与修改人值相同',
+    createtime datetime default CURRENT_TIMESTAMP null comment '创建时间,默认第一次创建的getdate()时间',
+    updatetime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间;第一次创建时与CreatTime值相同,修改时与修改时间相同'
+)
+    comment '邮件字段映射表';
+
+create table rz_market_operate_ppwfund.mo_email_file_info
+(
+    id         int auto_increment comment '主键Id'
+        primary key,
+    email_id   int                                not null comment '邮件id',
+    fund_id    int                                null comment '基金id',
+    file_name  varchar(255)                       not null comment '附件名称',
+    file_path  varchar(1024)                      not null comment '附件路径',
+    ai_parse   tinyint  default 0                 not null comment '是否利用ai工具来解析的',
+    ai_file_id varchar(64)                        null comment 'ai解析时上传的文件id(方便重新解析)',
+    isvalid    tinyint  default 1                 not null comment '记录的有效性;1-有效;0-无效',
+    creatorid  int                                not null comment '创建者Id',
+    updaterid  int                                not null comment '修改者Id',
+    createtime datetime default CURRENT_TIMESTAMP null comment '创建时间',
+    updatetime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间'
+)
+    comment '邮件附件信息表';
+
+create table rz_market_operate_ppwfund.mo_email_parse_info
+(
+    id           int auto_increment comment '主键Id'
+        primary key,
+    sender_email varchar(255)                       null comment '邮件发送方',
+    email        varchar(255)                       null comment '邮箱地址',
+    email_key    varchar(64)                        null,
+    email_date   datetime                           null comment '邮箱日期',
+    parse_date   datetime                           null comment '解析日期',
+    email_title  varchar(255)                       null comment '邮件主题',
+    email_type   int                                null,
+    parse_status int                                null comment '解析状态',
+    fail_reason  text                               null comment '失败原因',
+    isvalid      int      default 1                 null comment '记录的有效性;1-有效;0-无效;',
+    creatorid    int                                null comment '创建者Id;第一次创建时与Creator值相同,修改时与修改人值相同',
+    updaterid    int                                null comment '修改者Id;第一次创建时与Creator值相同,修改时与修改人值相同',
+    createtime   datetime default CURRENT_TIMESTAMP null comment '创建时间,默认第一次创建的getdate()时间',
+    updatetime   datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间;第一次创建时与CreatTime值相同,修改时与修改时间相同'
+)
+    comment '邮件解析信息表';
+
+create table rz_market_operate_ppwfund.mo_report_base_info
+(
+    id          int auto_increment comment '主键Id'
+        primary key,
+    file_id     int                                not null,
+    report_date datetime                           null comment '报告日期',
+    report_name varchar(255)                       null comment '报告名称',
+    report_type varchar(255)                       null comment '报告类型',
+    isvalid     int      default 1                 null comment '记录的有效性;1-有效;0-无效;',
+    creatorid   int                                null comment '创建者Id;第一次创建时与Creator值相同,修改时与修改人值相同',
+    updaterid   int                                null comment '修改者Id;第一次创建时与Creator值相同,修改时与修改人值相同',
+    createtime  datetime default CURRENT_TIMESTAMP null comment '创建时间,默认第一次创建的getdate()时间',
+    updatetime  datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间;第一次创建时与CreatTime值相同,修改时与修改时间相同'
+)
+    comment '报告基础信息表';
+
+create table rz_market_operate_ppwfund.mo_report_fund_info
+(
+    id           int auto_increment comment '主键,自动递增'
+        primary key,
+    file_id      int                                not null,
+    fund_name    varchar(255)                       not null comment '基金的名称',
+    fund_code    varchar(50)                        null comment '基金的唯一识别代码',
+    company_name varchar(255)                       null comment '基金管理人的名称',
+    currency     varchar(20)                        null comment '基金交易使用的货币种类',
+    isvalid      tinyint  default 1                 not null comment '记录的有效性;1-有效;0-无效',
+    creatorid    int                                not null comment '创建者Id',
+    updaterid    int                                not null comment '修改者Id',
+    createtime   datetime default CURRENT_TIMESTAMP null comment '创建时间',
+    updatetime   datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间'
+)
+    comment '报告解析结果之基金信息表';
+
+create table rz_market_operate_ppwfund.mo_report_investor_info
+(
+    id                 int auto_increment comment '主键,自动递增'
+        primary key,
+    file_id            int                                not null,
+    investor_name      varchar(255)                       not null comment '投资人的姓名',
+    investor_type      varchar(50)                        null comment '投资人的类别(例如:个人、机构)',
+    certificate_type   varchar(50)                        null comment '证件类型(例如:身份证、护照)',
+    certificate_number varchar(50)                        null comment '投资人证件号码',
+    fund_account       varchar(50)                        null comment '基金账户编号',
+    trading_account    varchar(50)                        null comment '投资者交易账号',
+    isvalid            tinyint  default 1                 not null comment '记录的有效性;1-有效;0-无效',
+    creatorid          int                                not null comment '创建者Id',
+    updaterid          int                                not null comment '修改者Id',
+    createtime         datetime default CURRENT_TIMESTAMP null comment '创建时间',
+    updatetime         datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间'
+)
+    comment '报告解析结果之投资者信息表';
+
+create table rz_market_operate_ppwfund.mo_report_fund_transaction
+(
+    id                        int auto_increment comment '主键Id'
+        primary key,
+    file_id                   int                                not null,
+    fund_account              varchar(100)                       null comment '基金账户编号',
+    fund_name                 varchar(100)                       null comment '基金名称',
+    distributor               varchar(100)                       null comment '销售此基金产品的经销商或银行',
+    transaction_type          varchar(50)                        null comment '业务类型(例如:申购、赎回)',
+    business_reason           varchar(100)                       null comment '业务操作的原因或类型说明',
+    status                    varchar(50)                        null comment '交易确认的状态(例如:已确认、待处理等)',
+    holding_date              date                               null comment '交易确认的日期',
+    apply_date                date                               null comment '申请的日期',
+    apply_amount              decimal(22, 6)                     null comment '申请的金额',
+    apply_share               decimal(22, 6)                     null comment '申请的基金份额数量',
+    amount                    decimal(22, 6)                     null comment '确认的金额',
+    share                     decimal(22, 6)                     null comment '确认的基金份额数量',
+    net_amount                decimal(22, 6)                     null comment '净认购/申购的金额,确认净额',
+    nav                       decimal(22, 6)                     null comment '单位净值',
+    confirmation_ratio        decimal(22, 6)                     null comment '确认比例',
+    ta_confirmation_number    varchar(100)                       null comment '交易授权确认编号',
+    ta_number                 varchar(100)                       null comment 'TA代码',
+    apply_no                  varchar(100)                       null comment '申请单号',
+    share_balance             decimal(22, 6)                     null comment '份额余额',
+    share_category            varchar(50)                        null comment '份额类别',
+    dividend_type             varchar(50)                        null comment '分红方式',
+    large_redemption_type     varchar(50)                        null comment '巨额赎回方式',
+    reward_mark               varchar(50)                        null comment '提成或保底标志',
+    holding_days              int                                null comment '持有天数',
+    share_registry_date       date                               null comment '份额明细注册日期',
+    fee                       decimal(22, 6)                     null comment '总费用',
+    interest                  decimal(22, 6)                     null comment '利息',
+    interest_to_fund_assets   decimal(22, 6)                     null comment '利息转份额/利息归基金资产',
+    trade_fee                 decimal(22, 6)                     null comment '交易费',
+    default_fee               decimal(22, 6)                     null comment '违约金',
+    performance_fee           decimal(22, 6)                     null comment '业绩报酬',
+    fee_discounts             decimal(22, 6)                     null comment '费用折扣',
+    performance_fee_discounts decimal(22, 6)                     null comment '业绩报酬折扣',
+    dividend_registry_date    date                               null comment '分红登记日',
+    dividend_payment_date     date                               null comment '红利发放日',
+    base_share_dividend       decimal(22, 6)                     null comment '分红基数份额',
+    dividend_mode             varchar(50)                        null comment '分红模式',
+    unit_dividend             decimal(22, 6)                     null comment '单位分红',
+    dividend_per_unit         decimal(22, 6)                     null comment '每单位分红',
+    total_dividend_amount     decimal(22, 6)                     null comment '红利总额',
+    actual_cash_dividend      decimal(22, 6)                     null comment '实发现金红利',
+    frozen_shares             decimal(22, 6)                     null comment '冻结份额',
+    frozen_amount             decimal(22, 6)                     null comment '冻结金额',
+    actual_performance_amount decimal(22, 6)                     null comment '实际业绩提成金额',
+    actual_performance_share  decimal(22, 6)                     null comment '实际提成份额',
+    isvalid                   int      default 1                 null comment '记录的有效性;1-有效;0-无效;',
+    creatorid                 int                                null comment '创建者Id;第一次创建时与Creator值相同,修改时与修改人值相同',
+    updaterid                 int                                null comment '修改者Id;第一次创建时与Creator值相同,修改时与修改人值相同',
+    createtime                datetime default CURRENT_TIMESTAMP null comment '创建时间,默认第一次创建的getdate()时间',
+    updatetime                datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间;第一次创建时与CreatTime值相同,修改时与修改时间相同'
+)
+    comment '交易流水表';
+
+
+-- 记得改邮箱配置
+INSERT INTO rz_market_operate_ppwfund.mo_mailbox_info (id, type, email, password, protocol, server, port, cron, open_status, description, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (1, 2, 'xx@simuwang.com', 'xx', 'imap', 'imap.exmail.qq.com', '993', null, 1, null, 1, null, null, '2025-02-22 16:03:21', '2025-02-22 16:17:48');
+
+
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (1, 'fundName', '基金名称,产品名称', 0, 1, null, null, '2025-02-20 15:54:06', '2025-02-20 15:54:06');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (2, 'fundCode', '产品代码,基金代码', 0, 1, null, null, '2025-02-20 15:54:06', '2025-02-20 15:54:06');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (3, 'companyName', '管理人,基金管理人', 0, 1, null, null, '2025-02-20 15:54:06', '2025-02-20 15:54:06');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (4, 'currency', '币种,货币种类', 4, 1, null, null, '2025-02-20 16:05:35', '2025-02-20 16:05:35');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (5, 'investorName', '投资人,客户名称,投资者名称,投资者姓名,投资者,投资人名称', 4, 1, null, null, '2025-02-20 16:05:35', '2025-02-22 09:12:16');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (6, 'investorType', '投资人类别,投资者类型,投资人类型', 4, 1, null, null, '2025-02-20 16:05:35', '2025-02-22 09:18:54');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (7, 'certificateType', '证件类型,投资者证件类型', 4, 1, null, null, '2025-02-20 16:05:35', '2025-02-20 16:05:35');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (8, 'certificateNumber', '证件号码,投资者证件号码', 4, 1, null, null, '2025-02-20 16:05:35', '2025-02-20 16:05:35');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (9, 'fundAccount', '基金账号,投资者基金账号', 4, 1, null, null, '2025-02-20 16:05:35', '2025-02-20 16:05:35');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (10, 'tradingAccount', '交易账号,投资者交易账号', 4, 1, null, null, '2025-02-20 16:05:35', '2025-02-20 16:05:35');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (11, 'distributor', '销售商,销售机构', 4, 1, null, null, '2025-02-20 16:05:35', '2025-02-20 16:05:35');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (12, 'nav', '单位净值,再投资单位净值,期末单位净值', 0, 1, null, null, '2025-02-20 17:17:20', '2025-02-22 09:41:48');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (13, 'transactionType', '业务类型', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-22 11:04:39');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (14, 'businessReason', '业务类型原因,失败原因,原因', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-20 18:08:10');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (15, 'status', '确认状态', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-20 18:08:10');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (16, 'holdingDate', '确认日期,交易确认日期', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-20 18:08:10');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (17, 'applyDate', '申请日期', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-20 18:08:10');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (18, 'applyAmount', '申请的金额,申请金额,申请金额(元)', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-22 11:04:39');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (19, 'applyShare', '申请的份额,申请份额,申请的基金份额数量', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-20 18:08:10');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (20, 'amount', '确认的金额,确认金额,确认金额(元),业绩报酬计提金额,再投资金额,再投资红利金额,原确认金额', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-22 11:04:39');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (21, 'share', '确认的份额,确认份额,扣减份额,再投资份额', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-22 11:04:38');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (22, 'netAmount', '净认购/申购的金额,确认净额', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-20 18:08:10');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (23, 'fee', '交易费,手续费用,交易费(元),手续费用(元),总费用,总费用(元),再投资费用', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-22 11:04:38');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (24, 'confirmationRatio', '确认比例', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-20 18:08:10');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (25, 'taConfirmationNumber', 'TA确认号,TA确认编号,确认单号,TA确认单号', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-22 11:04:38');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (27, 'interest', '利息,利息(元)', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-22 11:04:38');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (28, 'interestToFundAssets', '利息转份额,利息归基金资产', 4, 1, null, null, '2025-02-20 18:07:51', '2025-02-20 18:08:10');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (29, 'applyNo', '申请单号', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (30, 'shareBalance', '份额余额', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (31, 'shareCategory', '份额类别', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (32, 'largeRedemptionType', '巨额赎回方式', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (33, 'tradeFee', '交易费,交易费(元)', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (34, 'defaultFee', '违约金,违约金(元)', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (35, 'performanceFee', '业绩报酬,业绩报酬(元)', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (36, 'feeDiscounts', '费用折扣', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (37, 'performanceFeeDiscounts', '业绩报酬折扣', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (38, 'dividendRegistryDate', '分红登记日', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (39, 'dividendPaymentDate', '红利发放日,红利下发日', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (40, 'baseShareDividend', '分红基数份额,分红总份额', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (41, 'dividendMode', '分红模式', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (42, 'unitDividend', '单位分红', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (43, 'dividendPerUnit', '每单位分红', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (44, 'totalDividendAmount', '红利总额,分红总金额', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (45, 'actualCashDividend', '实发现金红利,实际所得现金红利', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (46, 'frozenShares', '冻结份额', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (47, 'frozenAmount', '冻结金额', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (48, 'actualPerformanceAmount', '实际业绩提成金额,实际提成金额', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (49, 'actualPerformanceShare', '实际提成份额,实际业绩提成份额', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (50, 'rewardMark', '提成或保底标志', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (51, 'holdingDays', '持有天数', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (52, 'shareRegistryDate', '份额明细注册日期', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (53, 'taNumber', 'TA代码', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');
+INSERT INTO rz_market_operate_ppwfund.mo_email_field_mapping (id, code, name, type, isvalid, creatorid, updaterid, createtime, updatetime) VALUES (54, 'dividendType', '分红方式', 4, 1, null, null, '2025-02-22 11:04:39', '2025-02-22 11:05:32');

+ 259 - 0
mo-daq/mvnw

@@ -0,0 +1,259 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.2
+#
+# Optional ENV vars
+# -----------------
+#   JAVA_HOME - location of a JDK home dir, required when download maven via java source
+#   MVNW_REPOURL - repo url base for downloading maven distribution
+#   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+#   MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+  [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+  native_path() { cygpath --path --windows "$1"; }
+  ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+  # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+  if [ -n "${JAVA_HOME-}" ]; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+      JAVACCMD="$JAVA_HOME/jre/sh/javac"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+      JAVACCMD="$JAVA_HOME/bin/javac"
+
+      if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+        echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+        echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+        return 1
+      fi
+    fi
+  else
+    JAVACMD="$(
+      'set' +e
+      'unset' -f command 2>/dev/null
+      'command' -v java
+    )" || :
+    JAVACCMD="$(
+      'set' +e
+      'unset' -f command 2>/dev/null
+      'command' -v javac
+    )" || :
+
+    if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+      echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+      return 1
+    fi
+  fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+  str="${1:-}" h=0
+  while [ -n "$str" ]; do
+    char="${str%"${str#?}"}"
+    h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+    str="${str#?}"
+  done
+  printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+  printf %s\\n "$1" >&2
+  exit 1
+}
+
+trim() {
+  # MWRAPPER-139:
+  #   Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+  #   Needed for removing poorly interpreted newline sequences when running in more
+  #   exotic environments such as mingw bash on Windows.
+  printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+  case "${key-}" in
+  distributionUrl) distributionUrl=$(trim "${value-}") ;;
+  distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+  esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+  MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+  case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+  *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+  :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+  :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+  :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+  *)
+    echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+    distributionPlatform=linux-amd64
+    ;;
+  esac
+  distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+  ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+  unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+  exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+  verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+  clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+  trap clean HUP INT TERM EXIT
+else
+  die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+  distributionUrl="${distributionUrl%.zip}.tar.gz"
+  distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+  verbose "Found wget ... using wget"
+  wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+  verbose "Found curl ... using curl"
+  curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+  verbose "Falling back to use Java to download"
+  javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+  targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+  cat >"$javaSource" <<-END
+	public class Downloader extends java.net.Authenticator
+	{
+	  protected java.net.PasswordAuthentication getPasswordAuthentication()
+	  {
+	    return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+	  }
+	  public static void main( String[] args ) throws Exception
+	  {
+	    setDefault( new Downloader() );
+	    java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+	  }
+	}
+	END
+  # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+  verbose " - Compiling Downloader.java ..."
+  "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+  verbose " - Running Downloader.java ..."
+  "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+  distributionSha256Result=false
+  if [ "$MVN_CMD" = mvnd.sh ]; then
+    echo "Checksum validation is not supported for maven-mvnd." >&2
+    echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+    exit 1
+  elif command -v sha256sum >/dev/null; then
+    if echo "$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
+      distributionSha256Result=true
+    fi
+  elif command -v shasum >/dev/null; then
+    if echo "$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+      distributionSha256Result=true
+    fi
+  else
+    echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+    echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+    exit 1
+  fi
+  if [ $distributionSha256Result = false ]; then
+    echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+    echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+    exit 1
+  fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+  unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+  tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"

+ 149 - 0
mo-daq/mvnw.cmd

@@ -0,0 +1,149 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.2
+@REM
+@REM Optional ENV vars
+@REM   MVNW_REPOURL - repo url base for downloading maven distribution
+@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+  IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+  $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+  Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+  "maven-mvnd-*" {
+    $USE_MVND = $true
+    $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+    $MVN_CMD = "mvnd.cmd"
+    break
+  }
+  default {
+    $USE_MVND = $false
+    $MVN_CMD = $script -replace '^mvnw','mvn'
+    break
+  }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+if ($env:MVNW_REPOURL) {
+  $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+  $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+  $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+  Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+  exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+  Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+  if ($TMP_DOWNLOAD_DIR.Exists) {
+    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+    catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+  }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+  if ($USE_MVND) {
+    Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+  }
+  Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+  if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+    Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+  }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+  Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+  if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+    Write-Error "fail to move MAVEN_HOME"
+  }
+} finally {
+  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+  catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

+ 204 - 0
mo-daq/pom.xml

@@ -0,0 +1,204 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>3.4.2</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+
+    <groupId>com.smppw</groupId>
+    <artifactId>mo-daq</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>mo-daq</name>
+    <description>mo-daq</description>
+
+    <properties>
+        <java.version>17</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+
+        <hutool.version>5.8.31</hutool.version>
+        <mybatis-plus-boot3.version>3.5.7</mybatis-plus-boot3.version>
+
+        <apahce-pdfbox.version>3.0.3</apahce-pdfbox.version>
+        <tabula.version>1.0.5</tabula.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-tomcat</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-undertow</artifactId>
+        </dependency>
+
+<!--        <dependency>-->
+<!--            <groupId>org.springframework.boot</groupId>-->
+<!--            <artifactId>spring-boot-starter-quartz</artifactId>-->
+<!--        </dependency>-->
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-mail</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.eclipse.angus</groupId>
+                    <artifactId>jakarta.mail</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.mail</groupId>
+            <artifactId>jakarta.mail</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.eclipse.angus</groupId>
+                    <artifactId>angus-mail</artifactId>
+                </exclusion>
+            </exclusions>
+            <version>2.0.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <version>${mybatis-plus-boot3.version}</version>
+        </dependency>
+
+        <!-- hutool 工具,按需引入 -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+
+        <!-- 压缩文件jar -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-compress</artifactId>
+            <version>1.27.1</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.junrar</groupId>
+            <artifactId>junrar</artifactId>
+            <version>7.5.1</version>
+        </dependency>
+        <dependency>
+            <groupId>net.sf.sevenzipjbinding</groupId>
+            <artifactId>sevenzipjbinding</artifactId>
+            <version>16.02-2.01</version>
+        </dependency>
+        <dependency>
+            <groupId>net.sf.sevenzipjbinding</groupId>
+            <artifactId>sevenzipjbinding-all-platforms</artifactId>
+            <version>16.02-2.01</version>
+        </dependency>
+
+        <!-- pdf解析 -->
+        <dependency>
+            <groupId>org.apache.pdfbox</groupId>
+            <artifactId>pdfbox</artifactId>
+            <version>${apahce-pdfbox.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-simple</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>technology.tabula</groupId>
+            <artifactId>tabula</artifactId>
+            <version>${tabula.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.pdfbox</groupId>
+                    <artifactId>pdfbox</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.pdfbox</groupId>
+                    <artifactId>pdfbox-io</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-simple</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+<!--        &lt;!&ndash; 通义千问 ai &ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>com.alibaba</groupId>-->
+<!--            <artifactId>dashscope-sdk-java</artifactId>-->
+<!--            <version>2.18.2</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>com.squareup.okio</groupId>-->
+<!--            <artifactId>okio</artifactId>-->
+<!--            <version>3.6.0</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>com.squareup.okhttp3</groupId>-->
+<!--            <artifactId>logging-interceptor</artifactId>-->
+<!--            <version>4.12.0</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>com.squareup.okhttp3</groupId>-->
+<!--            <artifactId>okhttp-sse</artifactId>-->
+<!--            <version>4.12.0</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>com.squareup.okhttp3</groupId>-->
+<!--            <artifactId>okhttp</artifactId>-->
+<!--            <version>4.12.0</version>-->
+<!--        </dependency>-->
+
+        <dependency>
+            <groupId>com.zaxxer</groupId>
+            <artifactId>HikariCP</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.graalvm.buildtools</groupId>
+                <artifactId>native-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 0 - 0
mo-daq/readme.md


+ 13 - 0
mo-daq/src/main/java/com/smppw/modaq/MoDaqApplication.java

@@ -0,0 +1,13 @@
+package com.smppw.modaq;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class MoDaqApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(MoDaqApplication.class, args);
+    }
+
+}

+ 30 - 0
mo-daq/src/main/java/com/smppw/modaq/application/api/ParseApi.java

@@ -0,0 +1,30 @@
+package com.smppw.modaq.application.api;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import com.smppw.modaq.application.service.EmailParseApiService;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Date;
+
+@RestController
+@RequestMapping("/v1/daq-parse")
+public class ParseApi {
+    private final EmailParseApiService service;
+
+    public ParseApi(EmailParseApiService service) {
+        this.service = service;
+    }
+
+    public void report(String startDateTime) {
+        Date now = new Date();
+        Date preDate;
+        if (StrUtil.isBlank(startDateTime)) {
+            preDate = DateUtil.offsetDay(now, -1);
+        } else {
+            preDate = DateUtil.parseDateTime(startDateTime);
+        }
+        this.service.parseEmail(preDate, now);
+    }
+}

+ 52 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/CustomPDFTextStripper.java

@@ -0,0 +1,52 @@
+package com.smppw.modaq.application.components;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.util.StrUtil;
+import org.apache.pdfbox.text.PDFTextStripper;
+import org.apache.pdfbox.text.TextPosition;
+
+import java.io.IOException;
+import java.util.List;
+
+import static com.smppw.modaq.common.conts.Constants.WATERMARK_REPLACE;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/12 14:00
+ * @description 自定义的文本去水印方法,发现水印基本是旋转文字并且比报告内其他文字都大
+ * @see CustomTabulaTextStripper 区别于表格文字去水印的实现
+ */
+public class CustomPDFTextStripper extends PDFTextStripper {
+    @Override
+    protected void writeString(String text, List<TextPosition> textPositions) throws IOException {
+        // 水印文字基本都是有角度的,统计有旋转角度的文字高度
+        List<Float> heights = ListUtil.list(false);
+        for (TextPosition textPosition : textPositions) {
+            float[][] values = textPosition.getTextMatrix().getValues();
+            double degrees = Math.toDegrees(Math.atan2(values[0][1], values[0][0]));
+            if (Math.abs(degrees % 90 - 0.01) > 0) {
+                heights.add(textPosition.getHeight());
+            }
+        }
+        // 集合为空表示text的内容没有水印影响,直接输出该内容
+        if (CollUtil.isEmpty(heights)) {
+            super.writeString(text);
+            return;
+        }
+        // 如果全是水印文字则直接去除
+        if (textPositions.size() == heights.size()) {
+            super.writeString(WATERMARK_REPLACE);
+            return;
+        }
+        // 否则去除水印(文字没有旋转角度,并且水印字体大小没有包含当前文字时说明是正常文字;否则识别为水印并用特殊符号代替)
+        List<String> newTexts = ListUtil.list(false);
+        for (TextPosition textPosition : textPositions) {
+            float[][] values = textPosition.getTextMatrix().getValues();
+            double degrees = Math.toDegrees(Math.atan2(values[0][1], values[0][0]));
+            float height = textPosition.getHeight();
+            newTexts.add(degrees == 0. && !heights.contains(height) ? textPosition.getUnicode() : WATERMARK_REPLACE);
+        }
+        super.writeString(String.join(StrUtil.EMPTY, newTexts));
+    }
+}

+ 194 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/CustomTabulaTextStripper.java

@@ -0,0 +1,194 @@
+package com.smppw.modaq.application.components;
+
+import cn.hutool.core.collection.ListUtil;
+import org.apache.fontbox.util.BoundingBox;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDFontDescriptor;
+import org.apache.pdfbox.pdmodel.font.PDType3Font;
+import org.apache.pdfbox.text.TextPosition;
+import technology.tabula.RectangleSpatialIndex;
+import technology.tabula.TextElement;
+import technology.tabula.TextStripper;
+import technology.tabula.Utils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/12 14:00
+ * @description 自定义的文本去水印方法,发现水印基本是旋转文字并且比报告内其他文字都大;主要依据文本旋转角度和字体大小判断是否为水印
+ */
+public class CustomTabulaTextStripper extends TextStripper {
+    private static final String NBSP = "\u00A0";
+    private static final float AVG_HEIGHT_MULT_THRESHOLD = 6.0f;
+    private static final float MAX_BLANK_FONT_SIZE = 40.0f;
+    private static final float MIN_BLANK_FONT_SIZE = 2.0f;
+    private final PDDocument document;
+    private final ArrayList<TextElement> textElements;
+    private final RectangleSpatialIndex<TextElement> spatialIndex;
+    private float minCharWidth = Float.MAX_VALUE;
+    private float minCharHeight = Float.MAX_VALUE;
+    private float totalHeight = 0.0f;
+    private int countHeight = 0;
+
+    public CustomTabulaTextStripper(PDDocument document, int pageNumber) throws IOException {
+        super(document, pageNumber);
+        this.document = document;
+        this.setStartPage(pageNumber);
+        this.setEndPage(pageNumber);
+        this.textElements = new ArrayList<>();
+        this.spatialIndex = new RectangleSpatialIndex<>();
+    }
+
+    public void process() throws IOException {
+        this.getText(this.document);
+    }
+
+    @Override
+    protected void writeString(String string, List<TextPosition> textPositions) {
+        // 有旋转角度的文字
+        List<TextPosition> rotationTexts = ListUtil.list(false);
+        for (TextPosition textPosition : textPositions) {
+            float[][] values = textPosition.getTextMatrix().getValues();
+            double degrees = Math.toDegrees(Math.atan2(values[0][1], values[0][0]));
+            if (!Objects.equals(degrees % 90, 0d)) {
+                rotationTexts.add(textPosition);
+            }
+        }
+        // 如果全是水印文字则直接去除
+        if (textPositions.size() == rotationTexts.size()) {
+            return;
+        }
+
+        // 其他场景需要写TextElement属性
+        for (TextPosition textPosition : textPositions) {
+            if (textPosition == null) {
+                continue;
+            }
+
+            String c = textPosition.getUnicode();
+
+            // if c not printable, return
+            if (!isPrintable(c)) {
+                continue;
+            }
+
+            float h = textPosition.getHeightDir();
+
+            if (c.equals(NBSP)) { // replace non-breaking space for space
+                c = " ";
+            }
+
+            // 文字没有旋转角度,并且水印字体大小没有包含当前文字时说明是正常文字
+            if (rotationTexts.contains(textPosition)) {
+                c = "";
+            }
+
+            float wos = textPosition.getWidthOfSpace();
+
+            TextElement te = new TextElement(Utils.round(textPosition.getYDirAdj() - h, 2),
+                    Utils.round(textPosition.getXDirAdj(), 2), Utils.round(textPosition.getWidthDirAdj(), 2),
+                    Utils.round(textPosition.getHeightDir(), 2), textPosition.getFont(), textPosition.getFontSizeInPt(), c,
+                    // workaround a possible bug in PDFBox:
+                    // https://issues.apache.org/jira/browse/PDFBOX-1755
+                    wos, textPosition.getDir());
+
+            this.minCharWidth = (float) Math.min(this.minCharWidth, te.getWidth());
+            this.minCharHeight = (float) Math.min(this.minCharHeight, te.getHeight());
+
+            countHeight++;
+            totalHeight += (float) te.getHeight();
+            float avgHeight = totalHeight / countHeight;
+
+            //We have an issue where tall blank cells throw off the row height calculation
+            //Introspect a blank cell a bit here to see if it should be thrown away
+            if ((te.getText() == null || te.getText().trim().isEmpty())) {
+                //if the cell height is more than AVG_HEIGHT_MULT_THRESHOLDxaverage, throw it away
+                if (avgHeight > 0
+                        && te.getHeight() >= (avgHeight * AVG_HEIGHT_MULT_THRESHOLD)) {
+                    continue;
+                }
+
+                //if the font size is outside of reasonable ranges, throw it away
+                if (textPosition.getFontSizeInPt() > MAX_BLANK_FONT_SIZE || textPosition.getFontSizeInPt() < MIN_BLANK_FONT_SIZE) {
+                    continue;
+                }
+            }
+
+            this.spatialIndex.add(te);
+            this.textElements.add(te);
+        }
+    }
+
+    @Override
+    protected float computeFontHeight(PDFont font) throws IOException {
+        BoundingBox bbox = font.getBoundingBox();
+        if (bbox.getLowerLeftY() < Short.MIN_VALUE) {
+            // PDFBOX-2158 and PDFBOX-3130
+            // files by Salmat eSolutions / ClibPDF Library
+            bbox.setLowerLeftY(-(bbox.getLowerLeftY() + 65536));
+        }
+        // 1/2 the bbox is used as the height todo: why?
+        float glyphHeight = bbox.getHeight() / 2;
+
+        // sometimes the bbox has very high values, but CapHeight is OK
+        PDFontDescriptor fontDescriptor = font.getFontDescriptor();
+        if (fontDescriptor != null) {
+            float capHeight = fontDescriptor.getCapHeight();
+            if (Float.compare(capHeight, 0) != 0 &&
+                    (capHeight < glyphHeight || Float.compare(glyphHeight, 0) == 0)) {
+                glyphHeight = capHeight;
+            }
+            // PDFBOX-3464, PDFBOX-448:
+            // sometimes even CapHeight has very high value, but Ascent and Descent are ok
+            float ascent = fontDescriptor.getAscent();
+            float descent = fontDescriptor.getDescent();
+            if (ascent > 0 && descent < 0 &&
+                    ((ascent - descent) / 2 < glyphHeight || Float.compare(glyphHeight, 0) == 0)) {
+                glyphHeight = (ascent - descent) / 2;
+            }
+        }
+
+        // transformPoint from glyph space -> text space
+        float height;
+        if (font instanceof PDType3Font) {
+            height = font.getFontMatrix().transformPoint(0, glyphHeight).y;
+        } else {
+            height = glyphHeight / 1000;
+        }
+
+        return height;
+    }
+
+    private boolean isPrintable(String s) {
+        char c;
+        Character.UnicodeBlock block;
+        boolean printable = false;
+        for (int i = 0; i < s.length(); i++) {
+            c = s.charAt(i);
+            block = Character.UnicodeBlock.of(c);
+            printable |= !Character.isISOControl(c) && block != null && block != Character.UnicodeBlock.SPECIALS;
+        }
+        return printable;
+    }
+
+    public List<TextElement> getTextElements() {
+        return this.textElements;
+    }
+
+    public RectangleSpatialIndex<TextElement> getSpatialIndex() {
+        return spatialIndex;
+    }
+
+    public float getMinCharWidth() {
+        return minCharWidth;
+    }
+
+    public float getMinCharHeight() {
+        return minCharHeight;
+    }
+}

+ 427 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/ReportParseUtils.java

@@ -0,0 +1,427 @@
+package com.smppw.modaq.application.components;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
+import com.smppw.modaq.common.enums.ReportParseStatus;
+import com.smppw.modaq.common.enums.ReportType;
+import com.smppw.modaq.common.exception.ReportParseException;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.text.PDFTextStripper;
+import technology.tabula.CustomObjectExtractor;
+import technology.tabula.Page;
+import technology.tabula.PageIterator;
+import technology.tabula.Table;
+import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
+
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+
+public final class ReportParseUtils {
+//    /**
+//     * 行业配置的表格列名称
+//     */
+//    public static final List<String> INDUSTRY_COLUMN_NAMES = ListUtil.list(false);
+//    /**
+//     * 份额变动的表格列名称
+//     */
+//    public static final List<String> SHARE_CHANGE_COLUMN_NAMES = ListUtil.list(false);
+//    /**
+//     * 主要财务指标识别列名称
+//     */
+//    public static final List<String> FINANCIAL_INDICATORS_COLUMN_NAMES = ListUtil.list(false);
+//    /**
+//     * 资产配置明细和大类关系映射
+//     */
+//    public static final Map<String, String> ASSET_ALLOCATION_TYPE_MAPPER = MapUtil.newHashMap(32, true);
+//
+//    static {
+//        // 财务指标
+//        FINANCIAL_INDICATORS_COLUMN_NAMES.add("期末基金净资产");
+//        FINANCIAL_INDICATORS_COLUMN_NAMES.add("期末基金资产净值");
+//
+//        FINANCIAL_INDICATORS_COLUMN_NAMES.add("报告期期末单位净值");
+//        FINANCIAL_INDICATORS_COLUMN_NAMES.add("期末基金份额净值");
+//
+//        FINANCIAL_INDICATORS_COLUMN_NAMES.add("本期利润");
+//        FINANCIAL_INDICATORS_COLUMN_NAMES.add("本期已实现收益");
+//        FINANCIAL_INDICATORS_COLUMN_NAMES.add("期末可供分配利润");
+//        FINANCIAL_INDICATORS_COLUMN_NAMES.add("期末可供分配基金份额利润");
+//        FINANCIAL_INDICATORS_COLUMN_NAMES.add("基金份额累计净值增长率");
+//
+//        // 中国证监会行业标准
+//        INDUSTRY_COLUMN_NAMES.add("农、林、牧、渔业");
+//        INDUSTRY_COLUMN_NAMES.add("采矿业");
+//        INDUSTRY_COLUMN_NAMES.add("制造业");
+//        INDUSTRY_COLUMN_NAMES.add("电力、热力、燃气及水生产和供应业");
+//        INDUSTRY_COLUMN_NAMES.add("建筑业");
+//        INDUSTRY_COLUMN_NAMES.add("批发和零售业");
+//        INDUSTRY_COLUMN_NAMES.add("交通运输、仓储和邮政业");
+//        INDUSTRY_COLUMN_NAMES.add("住宿和餐饮业");
+//        INDUSTRY_COLUMN_NAMES.add("信息传输、软件和信息技术服务业");
+//        INDUSTRY_COLUMN_NAMES.add("金融业");
+//        INDUSTRY_COLUMN_NAMES.add("房地产业");
+//        INDUSTRY_COLUMN_NAMES.add("租赁和商务服务业");
+//        INDUSTRY_COLUMN_NAMES.add("科学研究和技术服务业");
+//        INDUSTRY_COLUMN_NAMES.add("水利、环境和公共设施管理业");
+//        INDUSTRY_COLUMN_NAMES.add("居民服务、修理和其他服务业");
+//        INDUSTRY_COLUMN_NAMES.add("教育");
+//        INDUSTRY_COLUMN_NAMES.add("卫生和社会工作");
+//        INDUSTRY_COLUMN_NAMES.add("文化、体育和娱乐业");
+//        INDUSTRY_COLUMN_NAMES.add("综合");
+//
+//        INDUSTRY_COLUMN_NAMES.add("港股通");
+//
+//        // 以下为国际标准
+//        INDUSTRY_COLUMN_NAMES.add("能源");
+//        INDUSTRY_COLUMN_NAMES.add("原材料");
+//        INDUSTRY_COLUMN_NAMES.add("材料");
+//        INDUSTRY_COLUMN_NAMES.add("工业");
+//        INDUSTRY_COLUMN_NAMES.add("可选消费品");
+//        INDUSTRY_COLUMN_NAMES.add("非日常生活消费品");
+//        INDUSTRY_COLUMN_NAMES.add("必须消费品");
+//        INDUSTRY_COLUMN_NAMES.add("日常消费品");
+//        INDUSTRY_COLUMN_NAMES.add("医疗保健");
+//        INDUSTRY_COLUMN_NAMES.add("金融");
+//        INDUSTRY_COLUMN_NAMES.add("信息技术");
+//        INDUSTRY_COLUMN_NAMES.add("通讯服务");
+//        INDUSTRY_COLUMN_NAMES.add("电信服务");
+//        INDUSTRY_COLUMN_NAMES.add("公用事业");
+//        INDUSTRY_COLUMN_NAMES.add("房地产");
+//
+//        // 份额变动表格识别列
+//        SHARE_CHANGE_COLUMN_NAMES.add("报告期期初基金份额总额");
+//        SHARE_CHANGE_COLUMN_NAMES.add("减:报告期期间基金总赎回份额");
+//        SHARE_CHANGE_COLUMN_NAMES.add("期末基金总份额/期末基金实缴总额");
+//        SHARE_CHANGE_COLUMN_NAMES.add("报告期期间基金拆分变动份额");
+//        SHARE_CHANGE_COLUMN_NAMES.add("报告期期间基金总申购份额");
+//
+//        // 资产配置
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("银行存款", "现金类资产");
+//        // 境内未上市、未挂牌公司股权投资
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("股权投资", "境内未上市、未挂牌公司股权投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其中:优先股", "境内未上市、未挂牌公司股权投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其他股权类投资", "境内未上市、未挂牌公司股权投资");
+//        // 上市公司定向增发投资
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("上市公司定向增发股票投资", "上市公司定向增发投资");
+//        // 新三板投资
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("新三板挂牌企业投资", "新三板投资");
+//        // 境内证券投资规模
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("结算备付金", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("存出保证金", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("股票投资", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("债券投资", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其中:银行间市场债券", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其中:利率债", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其中:信用债", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("资产支持证券", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("基金投资(公募基金)", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其中:货币基金", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("期货及衍生品交易保证金", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("买入返售金融资产", "境内证券投资规模");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其他证券类标的", "境内证券投资规模");
+//        // 资管计划投资
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("商业银行理财产品投资", "资管计划投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("信托计划投资", "资管计划投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("基金公司及其子公司资产管理计划投资", "资管计划投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("保险资产管理计划投资", "资管计划投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("证券公司及其子公司资产管理计划投资", "资管计划投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("期货公司及其子公司资产管理计划投资", "资管计划投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("私募基金产品投资", "资管计划投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("未在协会备案的合伙企业份额", "资管计划投资");
+//        // 另类投资
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("另类投资", "另类投资");
+//        // 境内债权类投资
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("银行委托贷款规模", "境内债权类投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("信托贷款", "境内债权类投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("应收账款投资", "境内债权类投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("各类受(收)益权投资", "境内债权类投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("票据(承兑汇票等)投资", "境内债权类投资");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其他债权投资", "境内债权类投资");
+//        // 境外投资
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("境外投资", "境外投资");
+//        // 其他资产
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其他资产", "其他资产");
+//        // 基金负债情况
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("债券回购总额", "基金负债情况");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("融资、融券总额", "基金负债情况");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其中:融券总额", "基金负债情况");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("银行借款总额", "基金负债情况");
+//        ASSET_ALLOCATION_TYPE_MAPPER.put("其他融资总额", "基金负债情况");
+//    }
+
+    /**
+     * 数据清洗,替换圆括号,包含中文或英文的圆括号
+     *
+     * @param value /
+     * @return /
+     */
+    public static String cleaningValue(Object value) {
+        return cleaningValue(value, true);
+    }
+
+    /**
+     * 数据简单清洗,并全部转为字符串类型
+     *
+     * @param value              待清洗的数据
+     * @param replaceParentheses 是否替换圆括号
+     * @return /
+     */
+    public static String cleaningValue(Object value, boolean replaceParentheses) {
+        String fieldValue = StrUtil.toStringOrNull(value);
+        if (!StrUtil.isNullOrUndefined(fieldValue)) {
+            // 特殊字符替换,空格替换为空字符
+            fieldValue = fieldValue
+                    .replace("\r", StrUtil.EMPTY)
+                    .replace(";", ";")
+                    .replaceAll(" ", StrUtil.EMPTY);
+            if (replaceParentheses) {
+                // 正则表达式匹配中文括号及其内容,并替换为空字符串
+                fieldValue = Pattern.compile("[(|(][^)]*[)|)]").matcher(fieldValue).replaceAll(StrUtil.EMPTY);
+            }
+        }
+        // 如果仅有 “-” 该字段值为null
+        if (Objects.equals("-", fieldValue)) {
+            fieldValue = null;
+        }
+        return StrUtil.isBlank(fieldValue) ? null : fieldValue;
+    }
+
+    /**
+     * 匹配分级基金名称(并且把母基金追加到第一行)
+     *
+     * @param text 文本内容
+     * @return /
+     */
+    public static List<String> matchTieredFund(String text) {
+        List<String> matches = ListUtil.list(false);
+        if (StrUtil.isBlank(text)) {
+            matches.add("母基金");
+            return matches;
+        }
+        // 使用正则表达式查找匹配项
+        Pattern pattern = Pattern.compile("[A-F]级|基金[A-F]");
+        Matcher matcher = pattern.matcher(text);
+        // 收集所有匹配项
+        while (matcher.find()) {
+            matches.add(matcher.group());
+        }
+        // 提取字母并按字母顺序排序
+        List<String> levels = matches.stream()
+                .map(s -> s.replaceAll("[^A-F]", ""))
+                .distinct()
+                .sorted()
+                .map(letter -> letter + "级")
+                .collect(Collectors.toList());
+        levels.add(0, "母基金");
+        return levels;
+    }
+
+    /**
+     * 匹配报告日期
+     *
+     * @param string 文本内容
+     * @return 报告日期
+     */
+    public static String matchReportDate(String string) {
+        if (string == null) {
+            return null;
+        }
+        // 编译正则表达式模式
+        Pattern pat1 = Pattern.compile("(2\\d{3}).*([一二三四1234])季度");  // 2023年XXX3季度
+        Pattern pat2 = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");  // 2023-12-31
+        Pattern pat3 = Pattern.compile("(2\\d{3})年年度");  // 2023年年度
+        Pattern pat4 = Pattern.compile("(\\d{4})年(\\d{1,2})月");  // 2023年12月
+        Pattern pat5 = Pattern.compile("\\d{4}\\d{2}\\d{2}");  // 20231231
+        Pattern pat6 = Pattern.compile("(2\\d{3})年度");  // 2023年度
+        // 创建Matcher对象
+        Matcher matcher1 = pat1.matcher(string);
+        Matcher matcher2 = pat2.matcher(string);
+        Matcher matcher3 = pat3.matcher(string);
+        Matcher matcher4 = pat4.matcher(string);
+        Matcher matcher5 = pat5.matcher(string);
+        Matcher matcher6 = pat6.matcher(string);
+        // 尝试匹配
+        if (matcher1.find()) {
+            String year = matcher1.group(1);
+            String quarter = matcher1.group(2);
+            return switch (quarter) {
+                case "一", "1" -> year + "-03-31";
+                case "二", "2" -> year + "-06-30";
+                case "三", "3" -> year + "-09-30";
+                case "四", "4" -> year + "-12-31";
+                default -> null;
+            };
+        } else if (matcher2.find()) {
+            return matcher2.group();
+        } else if (matcher5.find()) {
+            return matcher5.group();
+        } else if (matcher3.find()) {
+            return matcher3.group(1) + "-12-31";
+        } else if (matcher6.find()) {
+            return matcher6.group(1) + "-12-31";
+        } else if (matcher4.find()) {
+            String year = matcher4.group(1);
+            String month = matcher4.group(2);
+            int lastDayOfMonth = getLastDayOfMonth(Integer.parseInt(year), Integer.parseInt(month));
+            return year + "-" + padZero(month) + "-" + padZero(lastDayOfMonth + "");
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 匹配报告类型,如“季度”、“年度”
+     *
+     * @param string 输入字符串
+     * @return 匹配到的报告类型子字符串,如果没有匹配到则返回null
+     */
+    public static ReportType matchReportType(String string) {
+        // 类型识别---先识别季度报告,没有季度再识别年度报告,最后识别月报
+        ReportType reportType = null;
+        if (StrUtil.containsAny(string, ReportType.QUARTERLY.getPatterns())) {
+            reportType = ReportType.QUARTERLY;
+        } else if (StrUtil.containsAny(string, ReportType.ANNUALLY.getPatterns())) {
+            reportType = ReportType.ANNUALLY;
+        } else if (StrUtil.containsAny(string, ReportType.MONTHLY.getPatterns())) {
+            reportType = ReportType.MONTHLY;
+        } else if (StrUtil.containsAny(string, ReportType.LETTER.getPatterns())) {
+            reportType = ReportType.LETTER;
+        }
+        return reportType;
+    }
+
+    private static int getLastDayOfMonth(int year, int month) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.YEAR, year);
+        calendar.set(Calendar.MONTH, month - 1); // Calendar.MONTH 是从0开始的
+        return calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
+    }
+
+    private static String padZero(String number) {
+        return String.format("%02d", Integer.parseInt(number));
+    }
+
+//    public static GenerationResult callWithMessage() throws ApiException, NoApiKeyException, InputRequiredException {
+//        Generation gen = new Generation();
+//        Message systemMsg = Message.builder()
+//                .role(Role.SYSTEM.getValue())
+//                .content("You are a helpful assistant.")
+//                .build();
+//        Message userMsg = Message.builder()
+//                .role(Role.USER.getValue())
+//                .content("你是谁?")
+//                .build();
+//        GenerationParam param = GenerationParam.builder()
+//                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
+//                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
+//                // 模型列表:https://help.aliyun.com/zh/model-studio/getting-started/models
+//                .model("qwen-plus")
+//                .messages(Arrays.asList(systemMsg, userMsg))
+//                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
+//                .build();
+//        return gen.call(param);
+//    }
+//
+//    public static void simpleMultiModalConversationCall()
+//            throws ApiException, NoApiKeyException, UploadFileException {
+//        MultiModalConversation conv = new MultiModalConversation();
+//        Map<String, Object> map = new HashMap<>();
+//        map.put("image", "./流水1.jpg");
+//        map.put("max_pixels", "1003520");
+//        map.put("min_pixels", "3136");
+//        MultiModalMessage userMessage = MultiModalMessage.builder().role(Role.USER.getValue())
+//                .content(Arrays.asList(
+//                        map,
+//                        // 目前为保证模型效果,模型内部会统一使用"Read all the text in the image."作为text的值,用户输入的文本不会生效。
+//                        Collections.singletonMap("text", "Read all the text in the image."))).build();
+//        MultiModalConversationParam param = MultiModalConversationParam.builder()
+//                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
+//                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
+//                // 模型列表:https://help.aliyun.com/zh/model-studio/getting-started/models
+//                .model("qwen-vl-ocr")
+//                .message(userMessage)
+//                .build();
+//        MultiModalConversationResult result = conv.call(param);
+//        System.out.println(JsonUtils.toJson(result));
+//    }
+//
+//    public static void main(String[] args) throws IOException {
+////        try {
+////            GenerationResult result = callWithMessage();
+////            System.out.println(result.getOutput().getChoices().get(0).getMessage().getContent());
+////        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
+////            System.err.println("错误信息:"+e.getMessage());
+////            System.out.println("请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code");
+////        }
+//        try {
+//            simpleMultiModalConversationCall();
+//        } catch (ApiException | NoApiKeyException | UploadFileException e) {
+//            System.out.println(e.getMessage());
+//        }
+//    }
+
+    public static void main(String[] args) throws IOException, ReportParseException {
+//        String filepath = "C:\\Users\\Administrator\\Desktop\\tmp\\(1)投资者交易确认函【申购】_【SZF635】佳岳国债增强私募证券投资基金_20250217_任军.pdf";
+//        String filepath = "C:\\Users\\Administrator\\Desktop\\tmp\\CP080A_优美利赢胜价值1号私募投资基金A_20250217_邓辉_申购确认_20250217131352.pdf";
+//        String filepath = "C:\\Users\\Administrator\\Desktop\\tmp\\宁水德远国债宝私募证券投资基金_青国平(S21002741743)_申购_20241213_基金交易确认单a2604e57e9a12108.sign.pdf";
+        String filepath = "C:\\Users\\Administrator\\Desktop\\tmp\\基金分红交易确认函_SJH876_2025-02-12_戴羽晨_202502130107720842.pdf";
+//        String filepath = "C:\\Users\\Administrator\\Desktop\\tmp\\SZN224_君之健睿泰私募证券投资基金_郑为民_20250214_申购确认_20250217102704.pdf";
+
+//        String aiParserUtl = "http://localhost:8088/upload-filepath";
+//
+//        Map<String, Object> params = MapUtil.newHashMap(4);
+//        params.put("filepath", filepath);
+//        String body = HttpUtil.get(aiParserUtl, params);
+//
+//        String content = "{" +
+//                StrUtil.subAfter(body, "{", false)
+//                        .replaceAll("\\\\", "")
+//                        .replaceAll("n", "")
+//                        .replaceAll(" ", "") +
+//                "}";
+//        System.out.println(content);
+
+//        List<String> textList;
+        // 解析报告和表格
+        try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(filepath))) {
+//            PDFTextStripper stripper1 = new PDFTextStripper();
+//            String text1 = stripper1.getText(document);
+
+//            // 识别所有文字(去水印后的)
+//            CustomPDFTextStripper stripper = new CustomPDFTextStripper();
+//            stripper.setSortByPosition(true);
+//            String text = stripper.getText(document).replace(Constants.WATERMARK_REPLACE, StrUtil.EMPTY);
+//            textList = StrUtil.split(text, System.lineSeparator());
+//            textList.removeIf(StrUtil::isBlank);
+//            if (textList.isEmpty()) {
+//                throw new ReportParseException(ReportParseStatus.REPORT_IS_SCAN, "");
+//            }
+            // 解析所有表格(单元格字符去水印)
+            List<Table> tables = ListUtil.list(true);
+//            BasicExtractionAlgorithm extractionAlgorithm = new BasicExtractionAlgorithm();
+            SpreadsheetExtractionAlgorithm spreadsheetExtractionAlgorithm = new SpreadsheetExtractionAlgorithm();
+            // 自定义表格提取工具,去除单元格中的水印文字
+            PageIterator pageIterator = new CustomObjectExtractor(document).extract();
+            while (pageIterator.hasNext()) {
+                Page page = pageIterator.next();
+                List<Table> tablesList = spreadsheetExtractionAlgorithm.extract(page);
+                tables.addAll(tablesList);
+            }
+            if (tables.isEmpty()) {
+                throw new ReportParseException(ReportParseStatus.REPORT_IS_SCAN, "");
+            }
+//            this.initTableInfo(tables);
+        }
+    }
+}

+ 128 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/AbstractReportParser.java

@@ -0,0 +1,128 @@
+package com.smppw.modaq.application.components.report.parser;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.smppw.modaq.application.components.ReportParseUtils;
+import com.smppw.modaq.common.enums.ReportParseStatus;
+import com.smppw.modaq.common.exception.ReportParseException;
+import com.smppw.modaq.domain.dto.report.BaseReportDTO;
+import com.smppw.modaq.domain.dto.report.ReportBaseInfoDTO;
+import com.smppw.modaq.domain.dto.report.ReportData;
+import com.smppw.modaq.domain.dto.report.ReportParserParams;
+import com.smppw.modaq.domain.entity.EmailFieldMappingDO;
+import com.smppw.modaq.domain.mapper.EmailFieldMappingMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/30 18:13
+ * @description 非python接口的报告解析抽象(主要是支持pdf、word和excel等格式)
+ */
+public abstract class AbstractReportParser<T extends ReportData> implements ReportParser<T> {
+    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final EmailFieldMappingMapper fieldMappingMapper;
+    /**
+     * 字段匹配规则
+     */
+    protected Map<String, String> fieldMapper;
+
+    public AbstractReportParser(EmailFieldMappingMapper fieldMappingMapper) {
+        this.fieldMappingMapper = fieldMappingMapper;
+        this.fieldMapper = MapUtil.newHashMap(128);
+    }
+
+    /**
+     * 初始化数据的方法
+     */
+    protected void init() {
+        List<EmailFieldMappingDO> emailFieldMapping = this.fieldMappingMapper.getEmailFieldMapping(ListUtil.of(3, 4));
+        if (CollUtil.isEmpty(emailFieldMapping)) {
+            throw new ReportParseException(ReportParseStatus.PARSE_RULE_NO_FUND);
+        }
+        for (EmailFieldMappingDO mapping : emailFieldMapping) {
+            String code = mapping.getCode();
+            List<String> names = StrUtil.split(mapping.getName(), ",");
+            for (String name : names) {
+                this.fieldMapper.putIfAbsent(name, code);
+            }
+        }
+    }
+
+    /**
+     * 数据清洗,默认啥也不做
+     *
+     * @param reportData 结果数据
+     */
+    protected abstract void cleaningReportData(T reportData);
+
+    /**
+     * 构建只有两列表格的dto数据对象
+     *
+     * @param <DTO>   泛型对象
+     * @param fileId  文件id
+     * @param clazz   泛型对象
+     * @param infoMap 表格转换的函数
+     * @return /
+     */
+    protected <DTO extends BaseReportDTO<?>> DTO buildDto(Integer fileId, Class<DTO> clazz, Map<String, Object> infoMap) {
+        try {
+            DTO dto = clazz.getDeclaredConstructor().newInstance();
+            dto.setFileId(fileId);
+            this.buildInfo(infoMap, dto);
+            return dto;
+        } catch (Exception ignored) {
+        }
+        return null;
+    }
+
+    /**
+     * 对象字段设置
+     *
+     * @param extInfoMap 名称与值的对应关系
+     * @param info       待设置的对象
+     */
+    protected void buildInfo(Map<String, Object> extInfoMap, Object info) {
+        if (MapUtil.isEmpty(extInfoMap)) {
+            return;
+        }
+        for (Map.Entry<String, Object> entry : extInfoMap.entrySet()) {
+            String k = ReportParseUtils.cleaningValue(entry.getKey());
+            String fieldValue = ReportParseUtils.cleaningValue(entry.getValue());
+            String fieldName = this.fieldMapper.get(k);
+            if (StrUtil.isBlank(fieldName)) {
+                continue;
+            }
+            try {
+                ReflectUtil.setFieldValue(info, fieldName, fieldValue);
+            } catch (Exception e) {
+                if (this.logger.isDebugEnabled()) {
+                    this.logger.debug("{} 字段值设置错误:{}", fieldName, e.getMessage());
+                }
+            }
+        }
+    }
+
+    /**
+     * 构建报告基本信息
+     *
+     * @param params /
+     * @return /
+     */
+    protected ReportBaseInfoDTO buildReportInfo(ReportParserParams params) {
+        Integer fileId = params.getFileId();
+        String reportName = params.getFilename();
+        ReportBaseInfoDTO reportInfo = new ReportBaseInfoDTO(fileId);
+        reportInfo.setReportName(reportName);
+        reportInfo.setReportType(params.getReportType().name());
+        reportInfo.setReportDate(ReportParseUtils.matchReportDate(reportName));
+        return reportInfo;
+    }
+}

+ 33 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/ReportParser.java

@@ -0,0 +1,33 @@
+package com.smppw.modaq.application.components.report.parser;
+
+import com.smppw.modaq.common.exception.ReportParseException;
+import com.smppw.modaq.domain.dto.report.ReportData;
+import com.smppw.modaq.domain.dto.report.ReportParserParams;
+
+import java.io.IOException;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/9 19:18
+ * @description 报告模板解析器,计划支持pdf、word等
+ */
+public interface ReportParser<T extends ReportData> {
+    /**
+     * 获取当前解析器名称
+     *
+     * @return /
+     */
+    default String getParser() {
+        return this.getClass().getSimpleName();
+    }
+
+    /**
+     * 报告模板解析接口
+     * 扩展支持月报、季报和年报,解析文件格式支持pdf、word和excel
+     *
+     * @param params 解析请求参数
+     * @return 解析结果
+     * @throws IOException 文件io异常
+     */
+    T parse(ReportParserParams params) throws IOException, ReportParseException;
+}

+ 63 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/ReportParserConstant.java

@@ -0,0 +1,63 @@
+package com.smppw.modaq.application.components.report.parser;
+
+import cn.hutool.core.map.MapUtil;
+import com.smppw.modaq.common.enums.ReportParserFileType;
+import com.smppw.modaq.common.enums.ReportType;
+
+import java.util.Map;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/29 13:39
+ * @description 报告解析的bean名称关系配置
+ */
+public final class ReportParserConstant {
+    public static final Map<ReportType, Map<ReportParserFileType, String>> REPORT_PARSER_BEAN_MAP = MapUtil.newHashMap(8);
+
+    // 交易流水确认函解析
+    public static final String PARSER_PDF_LETTER = "report-parser:pdf:letter";
+
+    public static final String PARSER_PDF_MONTHLY = "report-parser:pdf:monthly";
+    public static final String PARSER_WORD_MONTHLY = "report-parser:word:monthly";
+    public static final String PARSER_EXCEL_MONTHLY = "report-parser:excel:monthly";
+    public static final String PARSER_PYTHON_MONTHLY = "report-parser:python:monthly";
+
+    public static final String PARSER_PDF_QUARTERLY = "report-parser:pdf:quarterly";
+    public static final String PARSER_WORD_QUARTERLY = "report-parser:word:quarterly";
+    public static final String PARSER_EXCEL_QUARTERLY = "report-parser:excel:quarterly";
+    public static final String PARSER_PYTHON_QUARTERLY = "report-parser:python:quarterly";
+
+    public static final String PARSER_PDF_ANNUALLY = "report-parser:pdf:annually";
+    public static final String PARSER_WORD_ANNUALLY = "report-parser:word:annually";
+    public static final String PARSER_EXCEL_ANNUALLY = "report-parser:excel:annually";
+    public static final String PARSER_PYTHON_ANNUALLY = "report-parser:python:annually";
+
+    static {
+        // 交易流水确认函解析
+        REPORT_PARSER_BEAN_MAP.put(ReportType.LETTER, Map.of(ReportParserFileType.PDF, PARSER_PDF_LETTER));
+
+        REPORT_PARSER_BEAN_MAP.put(ReportType.MONTHLY,
+                Map.of(ReportParserFileType.PDF, PARSER_PDF_MONTHLY,
+                        ReportParserFileType.WORD, PARSER_WORD_MONTHLY,
+                        ReportParserFileType.EXCEL, PARSER_EXCEL_MONTHLY,
+
+                        ReportParserFileType.PYTHON, PARSER_PYTHON_MONTHLY
+                ));
+
+        REPORT_PARSER_BEAN_MAP.put(ReportType.QUARTERLY,
+                Map.of(ReportParserFileType.PDF, PARSER_PDF_QUARTERLY,
+                        ReportParserFileType.WORD, PARSER_WORD_QUARTERLY,
+                        ReportParserFileType.EXCEL, PARSER_EXCEL_QUARTERLY,
+
+                        ReportParserFileType.PYTHON, PARSER_PYTHON_QUARTERLY
+                ));
+
+        REPORT_PARSER_BEAN_MAP.put(ReportType.ANNUALLY,
+                Map.of(ReportParserFileType.PDF, PARSER_PDF_ANNUALLY,
+                        ReportParserFileType.WORD, PARSER_WORD_ANNUALLY,
+                        ReportParserFileType.EXCEL, PARSER_EXCEL_ANNUALLY,
+
+                        ReportParserFileType.PYTHON, PARSER_PYTHON_ANNUALLY
+                ));
+    }
+}

+ 32 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/ReportParserFactory.java

@@ -0,0 +1,32 @@
+package com.smppw.modaq.application.components.report.parser;
+
+import cn.hutool.core.map.MapUtil;
+import com.smppw.modaq.common.enums.ReportParseStatus;
+import com.smppw.modaq.common.enums.ReportParserFileType;
+import com.smppw.modaq.common.enums.ReportType;
+import com.smppw.modaq.common.exception.ReportParseException;
+import com.smppw.modaq.domain.dto.report.ReportData;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+@Component
+public class ReportParserFactory {
+    private static final ReportParser<? extends ReportData> DEFAULT = (ReportParser<ReportData>) params -> null;
+
+    private static final Map<String, ReportParser<? extends ReportData>> REPORT_WRITER_MAP = MapUtil.newHashMap(32);
+
+    public ReportParserFactory(Map<String, ReportParser<? extends ReportData>> components) {
+        REPORT_WRITER_MAP.putAll(components);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends ReportData> ReportParser<T> getInstance(ReportType reportType, ReportParserFileType reportParserFileType) {
+        String beanName = ReportParserConstant.REPORT_PARSER_BEAN_MAP.getOrDefault(reportType, MapUtil.empty()).get(reportParserFileType);
+        ReportParser<? extends ReportData> reportParser = REPORT_WRITER_MAP.get(beanName);
+        if (reportParser == null) {
+            throw new ReportParseException(ReportParseStatus.NO_SUPPORT_TEMPLATE);
+        }
+        return (ReportParser<T>) reportParser;
+    }
+}

+ 210 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/pdf/AbstractPDReportParser.java

@@ -0,0 +1,210 @@
+package com.smppw.modaq.application.components.report.parser.pdf;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.smppw.modaq.application.components.report.parser.AbstractReportParser;
+import com.smppw.modaq.common.enums.ReportParseStatus;
+import com.smppw.modaq.common.exception.ReportParseException;
+import com.smppw.modaq.domain.dto.report.ReportBaseInfoDTO;
+import com.smppw.modaq.domain.dto.report.ReportData;
+import com.smppw.modaq.domain.dto.report.ReportFundInfoDTO;
+import com.smppw.modaq.domain.dto.report.ReportParserParams;
+import com.smppw.modaq.domain.mapper.EmailFieldMappingMapper;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.springframework.beans.factory.annotation.Value;
+import technology.tabula.CustomObjectExtractor;
+import technology.tabula.Page;
+import technology.tabula.PageIterator;
+import technology.tabula.Table;
+import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/29 16:45
+ * @description pdf格式的报告解析抽象类
+ */
+public abstract class AbstractPDReportParser<T extends ReportData> extends AbstractReportParser<T> {
+    /**
+     * 基金信息表格
+     */
+    protected Table fundInfoTable;
+//    /**
+//     * 去除了水印的所有文本内容
+//     */
+//    protected List<String> textList;
+
+    @Value("${email.report.ai-parser-url}")
+    private String aiParserUrl;
+
+    protected String aiFileId;
+
+    protected String aiParserContent;
+
+    protected Boolean aiParse = false;
+
+    public AbstractPDReportParser(EmailFieldMappingMapper fieldMappingMapper) {
+        super(fieldMappingMapper);
+    }
+
+    @Override
+    public T parse(ReportParserParams params) throws IOException, ReportParseException {
+        // 先初始化为null
+        this.fundInfoTable = null;
+//        this.textList = null;
+        // 初始化
+        this.init();
+        String filename = params.getFilename();
+        String filepath = params.getFilepath();
+        // 解析报告和表格
+        try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(filepath))) {
+//            // 识别所有文字(去水印后的)
+//            CustomPDFTextStripper stripper = new CustomPDFTextStripper();
+//            stripper.setSortByPosition(true);
+//            String text = stripper.getText(document).replace(Constants.WATERMARK_REPLACE, StrUtil.EMPTY);
+//            this.textList = StrUtil.split(text, System.lineSeparator());
+//            this.textList.removeIf(StrUtil::isBlank);
+//            if (this.textList.isEmpty()) {
+//                throw new ReportParseException(ReportParseStatus.REPORT_IS_SCAN, filename);
+//            }
+            // 解析所有表格(单元格字符去水印)
+            List<Table> tables = ListUtil.list(true);
+            SpreadsheetExtractionAlgorithm spreadsheetExtractionAlgorithm = new SpreadsheetExtractionAlgorithm();
+            // 自定义表格提取工具,去除单元格中的水印文字
+            PageIterator pageIterator = new CustomObjectExtractor(document).extract();
+            while (pageIterator.hasNext()) {
+                Page page = pageIterator.next();
+                List<Table> tableList = spreadsheetExtractionAlgorithm.extract(page);
+                Integer rows = tableList.stream().map(Table::getRowCount).filter(rowCount -> rowCount >= 1).reduce(0, Integer::sum);
+                if (rows >= 1) {
+                    for (Table table : tableList) {
+                        int rowCount = table.getRowCount();
+                        if (rowCount >= 1) {
+                            tables.add(table);
+                        }
+                    }
+                } else {
+                    this.aiParse = true;
+                    Map<String, Object> paramsMap = MapUtil.newHashMap(4);
+                    paramsMap.put("filepath", filepath);
+                    paramsMap.put("file_id", params.getAiFileId());
+                    String body = null;
+                    try {
+                        body = HttpUtil.get(this.aiParserUrl, paramsMap);
+                        JSONObject jsonObject = JSONUtil.parseObj(body);
+                        this.aiFileId = MapUtil.getStr(jsonObject, "file_id");
+                        String content = StrUtil.split(jsonObject.getStr("content"), "```").get(1);
+                        this.aiParserContent = "{" + StrUtil.subAfter(content, "{", false) + "}";
+                    } catch (Exception e) {
+                        this.logger.warn("{} ai解析失败,解析结果{},错误原因:{}", filename, body, ExceptionUtil.stacktraceToString(e));
+                    }
+                }
+
+            }
+            if (tables.isEmpty() && StrUtil.isBlank(this.aiParserContent)) {
+                throw new ReportParseException(ReportParseStatus.REPORT_IS_SCAN, filename);
+            }
+            this.initTableInfo(tables);
+        }
+        try {
+            // 报告基本信息
+            ReportBaseInfoDTO reportInfo = this.buildReportInfo(params);
+            // 解析报告中主体基金的基本信息
+            ReportFundInfoDTO reportFundInfo = this.buildFundInfo(params);
+            // 解析其他表格信息并且设置结果字段
+            T reportData = this.parseExtInfoAndSetData(reportInfo, reportFundInfo);
+            // 数据清洗后返回
+            this.cleaningReportData(reportData);
+            return reportData;
+        } catch (ReportParseException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ReportParseException(ReportParseStatus.NOT_A_FIXED_FORMAT, filename);
+        }
+    }
+
+    /**
+     * 初始化解析所有表格数据
+     *
+     * @param tables 按固定的表格模式划分到不同的对象中
+     */
+    protected abstract void initTableInfo(List<Table> tables);
+
+    /**
+     * 绑定基金基本信息
+     *
+     * @param params /
+     * @return /
+     */
+    protected abstract ReportFundInfoDTO buildFundInfo(ReportParserParams params);
+
+    /**
+     * 解析报告的其他信息并设置到对象中
+     *
+     * @param reportInfo 报告基本信息
+     * @param fundInfo   报告中基金基本信息
+     * @return /
+     */
+    protected abstract T parseExtInfoAndSetData(ReportBaseInfoDTO reportInfo, ReportFundInfoDTO fundInfo);
+
+    @Override
+    protected void cleaningReportData(T reportData) {
+        // cleaning.
+    }
+
+//    /**
+//     * 构建只有两列表格的dto数据对象,如果有分级基金时(并且一个表格可能跨页)
+//     *
+//     * @param <DTO>    泛型对象
+//     * @param fileId   文件id
+//     * @param tables   表格
+//     * @param clazz    泛型对象
+//     * @param function 表格转换的函数
+//     * @return /
+//     */
+//    protected <DTO extends BaseReportLevelDTO<?>> List<DTO> buildLevelDto(Integer fileId, List<Table> tables, Class<DTO> clazz,
+//                                                                          Function<Table, Map<String, Object>> function) {
+//        List<DTO> dtos = ListUtil.list(true);
+//        // 信息表格字段和值映射
+//        List<Map<String, Object>> infos = ListUtil.list(true);
+//        Map<String, Object> infoMap = null;
+//        for (Table table : tables) {
+//            Map<String, Object> temp = function.apply(table);
+//            for (String key : temp.keySet()) {
+//                // 如果infoMap为null,先声明然后放在infos中
+//                if (infoMap == null) {
+//                    infoMap = MapUtil.newHashMap(16);
+//                    infos.add(infoMap);
+//                }
+//                // 如果infoMap中包含了该key时,先放infos中然后重新声明新map对象
+//                if (infoMap.containsKey(key)) {
+//                    infos.add(new HashMap<>(infoMap));
+//                    infoMap = MapUtil.newHashMap(16);
+//                } else {
+//                    infoMap.put(key, temp.get(key));
+//                }
+//            }
+//        }
+//        // 分级基金匹配
+//        List<String> levels = ReportParseUtils.matchTieredFund(String.join(",", this.textList));
+//        for (int i = 0; i < infos.size(); i++) {
+//            DTO dto = this.buildDto(fileId, clazz, infos.get(i));
+//            if (dto == null) {
+//                continue;
+//            }
+//            dto.setLevel(levels.get(i));
+//            dtos.add(dto);
+//        }
+//        return dtos;
+//    }
+}

+ 143 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/pdf/PDAnnuallyReportParser.java

@@ -0,0 +1,143 @@
+//package com.smppw.modaq.infrastructure.components.report.parser.pdf;
+//
+//import cn.hutool.core.collection.CollUtil;
+//import cn.hutool.core.collection.ListUtil;
+//import cn.hutool.core.map.MapUtil;
+//import com.smppw.modaq.domain.mapper.EmailFieldMappingMapper;
+//import com.smppw.modaq.infrastructure.components.ReportParseUtils;
+//import com.smppw.modaq.infrastructure.components.report.parser.ReportParserConstant;
+//import com.smppw.modaq.infrastructure.dto.report.AnnuallyReportData;
+//import com.smppw.modaq.infrastructure.dto.report.ReportFundInfoDTO;
+//import com.smppw.modaq.infrastructure.dto.report.ReportParserParams;
+//import org.springframework.stereotype.Component;
+//import technology.tabula.Table;
+//
+//import java.util.List;
+//import java.util.Map;
+//import java.util.Set;
+//import java.util.function.Function;
+//
+///**
+// * @author wangzaijun
+// * @date 2024/10/10 17:34
+// * @description 年报解析逻辑:基本信息被拆分为多个表格,财务报表未解析
+// */
+//@Component(ReportParserConstant.PARSER_PDF_ANNUALLY)
+//public class PDAnnuallyReportParser extends PDQuarterlyReportParser<AnnuallyReportData> {
+//    private List<Table> fundInfoTables;
+//
+//    public PDAnnuallyReportParser(EmailFieldMappingMapper fieldMappingMapper) {
+//        super(fieldMappingMapper);
+//    }
+//
+//    @Override
+//    public String getParser() {
+//        return ReportParserConstant.PARSER_PDF_ANNUALLY;
+//    }
+//
+//    @Override
+//    protected void initTableInfo(List<Table> tables) {
+//        // 初始化
+//        this.fundInfoTables = ListUtil.list(true);
+//        this.financialIndicatorsTables = ListUtil.list(true);
+//        this.shareChangeTables = ListUtil.list(true);
+//        this.assetAllocationTables = ListUtil.list(true);
+//        this.investmentIndustryTables = ListUtil.list(true);
+//        for (int i = 0; i < tables.size(); i++) {
+//            Table table = tables.get(i);
+//            if (i <= 1) {
+//                this.fundInfoTables.add(table);
+//                continue;
+//            }
+//            // 用表格的第一列的数据判断是否主要财务指标数据
+//            List<String> texts = this.getTableColTexts(table, 0);
+//            if (CollUtil.containsAny(texts, ReportParseUtils.FINANCIAL_INDICATORS_COLUMN_NAMES)) {
+//                this.financialIndicatorsTables.add(table);
+//                continue;
+//            }
+//            int colCount = table.getColCount();
+//            if (colCount == 2) {
+//                // 用表格的第一列的数据判断是否份额变动记录
+//                if (CollUtil.containsAny(texts, ReportParseUtils.SHARE_CHANGE_COLUMN_NAMES)) {
+//                    this.shareChangeTables.add(table);
+//                }
+//            } else if (colCount == 4) {
+//                // 用表格的第二列的数据判断是否行业配置数据(内地)
+//                texts = this.getTableColTexts(table, 1);
+//                if (CollUtil.containsAny(texts, ReportParseUtils.INDUSTRY_COLUMN_NAMES)) {
+//                    this.investmentIndustryTables.add(table);
+//                }
+//            } else if (colCount == 3) {
+//                // 用表格的第一列的数据判断是否行业配置数据(港股通)
+//                if (CollUtil.containsAny(texts, ReportParseUtils.INDUSTRY_COLUMN_NAMES)) {
+//                    this.investmentIndustryTables.add(table);
+//                    continue;
+//                }
+//                // 资产配置表格识别(兼容跨页的表格)获取表格中第二列的所有文字,判断所有文字中包含"股权投资"等字符串
+//                texts = this.getTableColTexts(table, 1);
+//                Set<String> keys = ReportParseUtils.ASSET_ALLOCATION_TYPE_MAPPER.keySet();
+//                if (CollUtil.containsAny(texts, keys)) {
+//                    this.assetAllocationTables.add(table);
+//                }
+//            }
+//        }
+//    }
+//
+//    @Override
+//    protected ReportFundInfoDTO buildFundInfo(ReportParserParams params) {
+//        Map<String, Object> fundInfoMap = MapUtil.newHashMap(32);
+//        for (Table table : this.fundInfoTables) {
+//            Map<String, Object> temp = this.parseFundInfo(table);
+//            fundInfoMap.putAll(temp);
+//        }
+//        ReportFundInfoDTO info = new ReportFundInfoDTO(params.getFileId());
+//        this.buildInfo(fundInfoMap, info);
+//        return info;
+//    }
+//
+//    @Override
+//    protected AnnuallyReportData buildReportData(ReportBaseInfoDTO reportInfo, ReportFundInfoDTO fundInfo,
+//                                                 List<ReportShareChangeDTO> shareChanges, List<ReportFinancialIndicatorsDTO> financialIndicators,
+//                                                 List<ReportAssetAllocationDTO> assetAllocations, List<ReportInvestmentIndustryDTO> investmentIndustries) {
+//        AnnuallyReportData reportData = new AnnuallyReportData(reportInfo, fundInfo);
+//        reportData.setShareChange(shareChanges);
+//        reportData.setFinancialIndicators(financialIndicators);
+//        reportData.setAssetAllocation(assetAllocations);
+//        reportData.setInvestmentIndustry(investmentIndustries);
+//        return reportData;
+//    }
+//
+//    @Override
+//    protected void cleaningReportData(AnnuallyReportData reportData) {
+//        // todo 数据清洗
+//    }
+//
+//    protected List<ReportFinancialIndicatorsDTO> buildFinancialIndicatorsInfo(Integer fileId, Function<Table, Map<String, Object>> function) {
+//        List<ReportFinancialIndicatorsDTO> dtos = ListUtil.list(false);
+//        // 分级基金
+//        List<String> levels = ReportParseUtils.matchTieredFund(String.join(",", this.textList));
+//        // 假设这里可能存在分级基金,不存在表格跨页
+//        for (int k = 0; k < this.financialIndicatorsTables.size(); k++) {
+//            Table table = this.financialIndicatorsTables.get(k);
+//            int colCount = table.getColCount();
+//            for (int j = 1; j < colCount; j++) {
+//                Map<String, Object> infoMap = MapUtil.newHashMap(16);
+//                String year = ReportParseUtils.cleaningValue(table.getCell(0, j).getText());
+//                infoMap.put("年度", year);
+//                for (int i = 0; i < table.getRowCount(); i++) {
+//                    String columnName = ReportParseUtils.cleaningValue(table.getCell(i, 0).getText());
+//                    if (columnName == null) {
+//                        continue;
+//                    }
+//                    String value = ReportParseUtils.cleaningValue(table.getCell(i, j).getText());
+//                    infoMap.put(columnName, value);
+//                }
+//                ReportFinancialIndicatorsDTO dto = new ReportFinancialIndicatorsDTO(fileId);
+//                this.buildInfo(infoMap, dto);
+//                dto.setLevel(levels.get(k));
+//                dtos.add(dto);
+//            }
+//        }
+//        return dtos;
+//    }
+//}

+ 92 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/pdf/PDLetterReportParser.java

@@ -0,0 +1,92 @@
+package com.smppw.modaq.application.components.report.parser.pdf;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.smppw.modaq.application.components.ReportParseUtils;
+import com.smppw.modaq.application.components.report.parser.ReportParserConstant;
+import com.smppw.modaq.domain.dto.report.*;
+import com.smppw.modaq.domain.mapper.EmailFieldMappingMapper;
+import org.springframework.stereotype.Component;
+import technology.tabula.Table;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@Component(ReportParserConstant.PARSER_PDF_LETTER)
+public class PDLetterReportParser extends AbstractPDReportParser<LetterReportData> {
+    private final Map<String, Object> allInfoMap = MapUtil.newHashMap(32);
+
+    public PDLetterReportParser(EmailFieldMappingMapper fieldMappingMapper) {
+        super(fieldMappingMapper);
+    }
+
+    @Override
+    protected void initTableInfo(List<Table> tables) {
+        // 每次重新清空map数据
+        this.allInfoMap.clear();
+        if (CollUtil.isEmpty(tables)) {
+            if (StrUtil.isNotBlank(this.aiParserContent)) {
+                JSONObject jsonObject = JSONUtil.parseObj(this.aiParserContent);
+                this.allInfoMap.putAll(flattenMap(jsonObject, ListUtil.list(false)));
+            }
+            return;
+        }
+        Table table = tables.get(0);
+        if (table == null) {
+            return;
+        }
+        int rowCount = table.getRowCount();
+        int colCount = table.getColCount();
+        for (int i = 0; i < rowCount; i++) {
+            int t = colCount / 2;
+            for (int j = 0; j < t; j++) {
+                String key = table.getCell(i, j * 2).getText().replaceAll("[a-zA-Z]", "");
+                key = ReportParseUtils.cleaningValue(key);
+                if (StrUtil.isBlank(key)) {
+                    continue;
+                }
+                this.allInfoMap.put(key, ReportParseUtils.cleaningValue(table.getCell(i, j * 2 + 1).getText()));
+            }
+        }
+    }
+
+    @Override
+    protected ReportFundInfoDTO buildFundInfo(ReportParserParams params) {
+        return this.buildDto(params.getFileId(), ReportFundInfoDTO.class, this.allInfoMap);
+    }
+
+    @Override
+    protected LetterReportData parseExtInfoAndSetData(ReportBaseInfoDTO reportInfo, ReportFundInfoDTO fundInfo) {
+        Integer fileId = reportInfo.getFileId();
+        // 投资者信息
+        ReportInvestorInfoDTO investorInfo = this.buildDto(fileId, ReportInvestorInfoDTO.class, this.allInfoMap);
+        // 交易流水
+        ReportFundTransactionDTO fundTransaction = this.buildDto(fileId, ReportFundTransactionDTO.class, this.allInfoMap);
+        // 构建结果数据
+        LetterReportData reportData = new LetterReportData(reportInfo, fundInfo);
+        reportData.setInvestorInfo(investorInfo);
+        reportData.setFundTransaction(fundTransaction);
+        reportData.setAiParse(Objects.equals(true, this.aiParse));
+        reportData.setAiFileId(this.aiFileId);
+        return reportData;
+    }
+
+    private static Map<String, Object> flattenMap(Map<String, Object> data, List<String> keys) {
+        Map<String, Object> result = MapUtil.newHashMap(16);
+        for (Map.Entry<String, Object> entry : data.entrySet()) {
+            List<String> currKeys = ListUtil.toList(keys);
+            currKeys.add(entry.getKey());
+            if (entry.getValue() instanceof Map<?, ?>) {
+                result.putAll(flattenMap((Map<String, Object>) entry.getValue(), currKeys));
+            } else {
+                result.put(entry.getKey(), entry.getValue());
+            }
+        }
+        return result;
+    }
+}

+ 89 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/pdf/PDMonthlyReportParser.java

@@ -0,0 +1,89 @@
+//package com.smppw.modaq.infrastructure.components.report.parser.pdf;
+//
+//import cn.hutool.core.map.MapUtil;
+//import com.smppw.modaq.domain.mapper.EmailFieldMappingMapper;
+//import com.smppw.modaq.infrastructure.components.report.parser.ReportParserConstant;
+//import com.smppw.modaq.infrastructure.dto.report.MonthlyReportData;
+//import com.smppw.modaq.infrastructure.dto.report.ReportBaseInfoDTO;
+//import com.smppw.modaq.infrastructure.dto.report.ReportFundInfoDTO;
+//import org.springframework.stereotype.Component;
+//import technology.tabula.RectangularTextContainer;
+//import technology.tabula.Table;
+//
+//import java.util.List;
+//import java.util.Map;
+//
+///**
+// * @author wangzaijun
+// * @date 2024/9/11 16:19
+// * @description pdf格式的月报解析
+// */
+//@Component(ReportParserConstant.PARSER_PDF_MONTHLY)
+//public class PDMonthlyReportParser extends AbstractPDReportParser<MonthlyReportData> {
+////    private List<Table> extNavTables;
+//
+//    public PDMonthlyReportParser(EmailFieldMappingMapper fieldMappingMapper) {
+//        super(fieldMappingMapper);
+//    }
+//
+//    @Override
+//    public String getParser() {
+//        return ReportParserConstant.PARSER_PDF_MONTHLY;
+//    }
+//
+//    @Override
+//    protected void initTableInfo(List<Table> tables) {
+////        // 这里初始化
+////        this.extNavTables = ListUtil.list(true);
+////        // 一般月报是固定的模板,4列表格是基金基本信息,其他5列的表格是月净值
+////        for (Table table : tables) {
+////            int colCount = table.getColCount();
+////            int rowCount = table.getRowCount();
+////            if (colCount == 0 && rowCount == 0) {
+////                continue;
+////            }
+////            if (colCount == 4) {
+////                this.fundInfoTable = table;
+////            } else if (colCount >= 5) {
+////                this.extNavTables.add(table);
+////            }
+////        }
+//    }
+//
+//    @Override
+//    protected Map<String, Object> parseFundInfo(Table fundInfoTable) {
+//        // 月报的基金基本信息是四列的表格
+//        Map<String, Object> baseInfoMap = MapUtil.newHashMap(32);
+//        for (int i = 0; i < fundInfoTable.getRows().size(); i++) {
+//            @SuppressWarnings("all")
+//            List<RectangularTextContainer> cols = fundInfoTable.getRows().get(i);
+//            for (int j = 0; j < 2; j++) {
+//                baseInfoMap.put(cols.get(j * 2).getText(), cols.get(j * 2 + 1).getText());
+//            }
+//        }
+//        return baseInfoMap;
+//    }
+//
+//    @Override
+//    protected MonthlyReportData parseExtInfoAndSetData(ReportBaseInfoDTO reportInfo, ReportFundInfoDTO fundInfo) {
+//        MonthlyReportData reportData = new MonthlyReportData(reportInfo, fundInfo);
+////        // 母基金和分级基金的净值
+////        List<ReportNetReportDTO> dtos = this.buildLevelDto(reportInfo.getFileId(), this.extNavTables,
+////                ReportNetReportDTO.class, t -> {
+////                    Map<String, Object> extInfoMap = MapUtil.newHashMap(16);
+////                    for (int i = 0; i < t.getColCount(); i++) {
+////                        String key = t.getCell(0, i).getText();
+////                        String value = t.getCell(1, i).getText();
+////                        extInfoMap.put(key, value);
+////                    }
+////                    return extInfoMap;
+////                });
+////        reportData.setNetReport(dtos);
+//        return reportData;
+//    }
+//
+//    @Override
+//    protected void cleaningReportData(MonthlyReportData reportData) {
+//        // todo 数据清洗
+//    }
+//}

+ 266 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/parser/pdf/PDQuarterlyReportParser.java

@@ -0,0 +1,266 @@
+//package com.smppw.modaq.infrastructure.components.report.parser.pdf;
+//
+//import cn.hutool.core.collection.CollUtil;
+//import cn.hutool.core.collection.ListUtil;
+//import cn.hutool.core.map.MapUtil;
+//import cn.hutool.core.util.StrUtil;
+//import com.simuwang.base.mapper.EmailFieldMappingMapper;
+//import com.simuwang.base.pojo.dto.report.*;
+//import com.simuwang.daq.components.ReportParseUtils;
+//import com.simuwang.daq.components.report.parser.ReportParserConstant;
+//import org.springframework.stereotype.Component;
+//import technology.tabula.RectangularTextContainer;
+//import technology.tabula.Table;
+//
+//import java.awt.geom.Rectangle2D;
+//import java.util.Comparator;
+//import java.util.List;
+//import java.util.Map;
+//import java.util.Set;
+//import java.util.function.Function;
+//
+///**
+// * @author wangzaijun
+// * @date 2024/9/29 17:53
+// * @description pdf格式的季报解析逻辑
+// */
+//@Component(ReportParserConstant.PARSER_PDF_QUARTERLY)
+//public class PDQuarterlyReportParser<T extends QuarterlyReportData> extends AbstractPDReportParser<T> {
+//    protected List<Table> financialIndicatorsTables;
+//    protected List<Table> shareChangeTables;
+//    protected List<Table> assetAllocationTables;
+//    protected List<Table> investmentIndustryTables;
+//
+//    public PDQuarterlyReportParser(EmailFieldMappingMapper fieldMappingMapper) {
+//        super(fieldMappingMapper);
+//    }
+//
+//    @Override
+//    public String getParser() {
+//        return ReportParserConstant.PARSER_PDF_QUARTERLY;
+//    }
+//
+//    @Override
+//    protected void initTableInfo(List<Table> tables) {
+//        this.financialIndicatorsTables = ListUtil.list(true);
+//        this.shareChangeTables = ListUtil.list(true);
+//        this.assetAllocationTables = ListUtil.list(true);
+//        this.investmentIndustryTables = ListUtil.list(true);
+//        for (Table table : tables) {
+//            int colCount = table.getColCount();
+//            int rowCount = table.getRowCount();
+//            if (colCount == 0 && rowCount == 0) {
+//                continue;
+//            }
+//            if (rowCount == 13 && colCount == 2) {
+//                this.fundInfoTable = table;
+//            } else if (colCount == 2) {
+//                // 用表格的第一列的数据判断是否份额变动记录
+//                List<String> texts = this.getTableColTexts(table, 0);
+//                // 主要财务指标或份额变动
+//                if (CollUtil.containsAny(texts, ReportParseUtils.SHARE_CHANGE_COLUMN_NAMES)) {
+//                    this.shareChangeTables.add(table);
+//                } else if (CollUtil.containsAny(texts, ReportParseUtils.FINANCIAL_INDICATORS_COLUMN_NAMES)) {
+//                    this.financialIndicatorsTables.add(table);
+//                }
+//            } else if (colCount == 4) {
+//                // 行业配置
+//                this.investmentIndustryTables.add(table);
+//            } else if (colCount == 3) {
+//                // 用表格的第一列单元格判断是否资产配置表
+//                List<String> texts = this.getTableColTexts(table, 0);
+//                if (CollUtil.containsAny(texts, ReportParseUtils.INDUSTRY_COLUMN_NAMES)) {
+//                    this.investmentIndustryTables.add(table);
+//                } else {
+//                    texts = this.getTableColTexts(table, 1);
+//                    Set<String> keys = ReportParseUtils.ASSET_ALLOCATION_TYPE_MAPPER.keySet();
+//                    if (CollUtil.containsAny(texts, keys)) {
+//                        this.assetAllocationTables.add(table);
+//                    }
+//                }
+//            }
+//        }
+//    }
+//
+//    @Override
+//    protected Map<String, Object> parseFundInfo(Table fundInfoTable) {
+//        // 季报和年报的基金基本信息是两列的表格
+//        Map<String, Object> baseInfoMap = MapUtil.newHashMap(32);
+//        for (int i = 0; i < fundInfoTable.getRows().size(); i++) {
+//            @SuppressWarnings("all")
+//            List<RectangularTextContainer> cols = fundInfoTable.getRows().get(i);
+//            for (int j = 0; j < 1; j++) {
+//                baseInfoMap.put(cols.get(j).getText(), cols.get(j + 1).getText());
+//            }
+//        }
+//        return baseInfoMap;
+//    }
+//
+//    @Override
+//    protected T parseExtInfoAndSetData(ReportBaseInfoDTO reportInfo, ReportFundInfoDTO fundInfo) {
+//        Integer fileId = reportInfo.getFileId();
+//        // 表格转换数据获取函数
+//        Function<Table, Map<String, Object>> function = t -> {
+//            Map<String, Object> extInfoMap = MapUtil.newHashMap(16);
+//            for (int i = 0; i < t.getRowCount(); i++) {
+//                String key = t.getCell(i, 0).getText();
+//                String value = t.getCell(i, 1).getText();
+//                extInfoMap.put(key, value);
+//            }
+//            return extInfoMap;
+//        };
+//        // 份额变动
+//        List<ReportShareChangeDTO> shareChanges = this.buildLevelDto(fileId, this.shareChangeTables,
+//                ReportShareChangeDTO.class, function);
+//        // 主要财务指标
+//        List<ReportFinancialIndicatorsDTO> financialIndicators = this.buildFinancialIndicatorsInfo(fileId, function);
+//        // 资产配置
+//        List<ReportAssetAllocationDTO> assetAllocations = this.buildAssetAllocationInfo(fileId);
+//        // 行业配置
+//        List<ReportInvestmentIndustryDTO> investmentIndustries = this.buildInvestmentIndustryInfo(fileId);
+//        // 返回数据构建
+//        return this.buildReportData(reportInfo, fundInfo, shareChanges, financialIndicators, assetAllocations, investmentIndustries);
+//    }
+//
+//    /**
+//     * 主要财务指标数据构建(包括分级基金,并且一个表格可能跨页)
+//     *
+//     * @param fileId   文件id
+//     * @param function 字段映射关系
+//     * @return /
+//     */
+//    protected List<ReportFinancialIndicatorsDTO> buildFinancialIndicatorsInfo(Integer fileId, Function<Table, Map<String, Object>> function) {
+//        return this.buildLevelDto(fileId, this.financialIndicatorsTables, ReportFinancialIndicatorsDTO.class, function);
+//    }
+//
+//    /**
+//     * 子类重写,放在cast异常
+//     *
+//     * @param reportInfo           报告基本信息
+//     * @param fundInfo             基金基本信息
+//     * @param shareChanges         份额变动
+//     * @param financialIndicators  基本财务指标
+//     * @param assetAllocations     资产配置
+//     * @param investmentIndustries 行业配置
+//     * @return /
+//     */
+//    protected T buildReportData(ReportBaseInfoDTO reportInfo, ReportFundInfoDTO fundInfo,
+//                                List<ReportShareChangeDTO> shareChanges,
+//                                List<ReportFinancialIndicatorsDTO> financialIndicators,
+//                                List<ReportAssetAllocationDTO> assetAllocations,
+//                                List<ReportInvestmentIndustryDTO> investmentIndustries) {
+//        QuarterlyReportData reportData = new QuarterlyReportData(reportInfo, fundInfo);
+//        reportData.setShareChange(shareChanges);
+//        reportData.setFinancialIndicators(financialIndicators);
+//        reportData.setAssetAllocation(assetAllocations);
+//        reportData.setInvestmentIndustry(investmentIndustries);
+//        @SuppressWarnings("unchecked")
+//        T t = (T) reportData;
+//        return t;
+//    }
+//
+//    @Override
+//    protected void cleaningReportData(T reportData) {
+//        // todo 数据清洗
+//    }
+//
+//    /**
+//     * 构建基金行业配置解析数据
+//     *
+//     * @return /
+//     */
+//    private List<ReportInvestmentIndustryDTO> buildInvestmentIndustryInfo(Integer fileId) {
+//        List<ReportInvestmentIndustryDTO> dtos = ListUtil.list(false);
+//        for (Table table : this.investmentIndustryTables) {
+//            int colCount = table.getColCount();
+//            // 投资地区: 1-境内, 2-港股通
+//            int investType = colCount == 4 ? 1 : 2;
+//            int j = colCount == 4 ? 1 : 0;
+//            // 按行遍历
+//            for (int i = 0; i < table.getRowCount(); i++) {
+//                String text = ReportParseUtils.cleaningValue(table.getCell(i, 0).getText());
+//                if (StrUtil.containsAny(text, "序号", "行业类别")) {
+//                    continue;
+//                }
+//                String industryName = ReportParseUtils.cleaningValue(table.getCell(i, j).getText());
+//                if (StrUtil.isBlank(industryName) || !ReportParseUtils.INDUSTRY_COLUMN_NAMES.contains(industryName)) {
+//                    continue;
+//                }
+//                ReportInvestmentIndustryDTO dto = new ReportInvestmentIndustryDTO(fileId);
+//                dto.setInvestType(investType);
+//                dto.setIndustryName(industryName);
+//                dto.setMarketValue(ReportParseUtils.cleaningValue(table.getCell(i, j + 1).getText()));
+//                dto.setRatio(ReportParseUtils.cleaningValue(table.getCell(i, j + 2).getText()));
+//                dtos.add(dto);
+//            }
+//        }
+//        return dtos;
+//    }
+//
+//    /**
+//     * 构建基金资产配置解析数据
+//     *
+//     * @param fileId 文件id
+//     * @return /
+//     */
+//    private List<ReportAssetAllocationDTO> buildAssetAllocationInfo(Integer fileId) {
+//        List<ReportAssetAllocationDTO> dtos = ListUtil.list(false);
+//        for (Table table : this.assetAllocationTables) {
+//            // 按行遍历
+//            for (@SuppressWarnings("all") List<RectangularTextContainer> row : table.getRows()) {
+//                // x坐标升序(防止部分行乱序问题)
+//                row.sort(Comparator.comparing(Rectangle2D.Float::getX));
+//                // 金额、市值,有时是 “备注#金额”的格式
+//                String marketValueAndRemark = ReportParseUtils.cleaningValue(row.get(2).getText());
+//                // 资产明细
+//                String detail = ReportParseUtils.cleaningValue(row.get(1).getText(), false);
+//                if (!ReportParseUtils.ASSET_ALLOCATION_TYPE_MAPPER.containsKey(detail)) {
+//                    continue;
+//                }
+//                // 大类
+//                String assetType = ReportParseUtils.ASSET_ALLOCATION_TYPE_MAPPER.get(detail);
+//                if (StrUtil.contains(marketValueAndRemark, "#")) {
+//                    // 有#表示有备注,而且可能有多个,多个用分号分隔的.
+//                    List<String> marketValueAndRemarks = StrUtil.split(marketValueAndRemark, ";");
+//                    for (String mr : marketValueAndRemarks) {
+//                        if (StrUtil.isBlank(mr)) {
+//                            continue;
+//                        }
+//                        List<String> mrs = StrUtil.split(mr, "#");
+//                        ReportAssetAllocationDTO dto = new ReportAssetAllocationDTO(fileId);
+//                        dto.setAssetType(assetType);
+//                        dto.setAssetDetails(detail);
+//                        dto.setMarketValue(mrs.get(1));
+//                        dto.setRemark(mrs.get(0));
+//                        dtos.add(dto);
+//                    }
+//                } else {
+//                    ReportAssetAllocationDTO dto = new ReportAssetAllocationDTO(fileId);
+//                    dto.setAssetType(assetType);
+//                    dto.setAssetDetails(detail);
+//                    dto.setMarketValue(marketValueAndRemark);
+//                    dtos.add(dto);
+//                }
+//            }
+//        }
+//        return dtos;
+//    }
+//
+//    /**
+//     * 获取表格指定列的所有文字内容
+//     *
+//     * @param table 表格
+//     * @param col   指定列
+//     * @return /
+//     */
+//    protected List<String> getTableColTexts(Table table, Integer col) {
+//        List<String> details = ListUtil.list(false);
+//        for (@SuppressWarnings("all") List<RectangularTextContainer> row : table.getRows()) {
+//            String detail = ReportParseUtils.cleaningValue(row.get(col).getText(), false);
+//            if (StrUtil.isNotBlank(detail)) {
+//                details.add(detail);
+//            }
+//        }
+//        return details;
+//    }
+//}

+ 52 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/AbstractReportWriter.java

@@ -0,0 +1,52 @@
+package com.smppw.modaq.application.components.report.writer;
+
+import com.smppw.modaq.domain.dto.report.ReportBaseInfoDTO;
+import com.smppw.modaq.domain.dto.report.ReportData;
+import com.smppw.modaq.domain.dto.report.ReportFundInfoDTO;
+import com.smppw.modaq.domain.mapper.ReportBaseInfoMapper;
+import com.smppw.modaq.domain.mapper.ReportFundInfoMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.transaction.annotation.Transactional;
+
+public abstract class AbstractReportWriter<T extends ReportData> implements ReportWriter<T> {
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final ReportBaseInfoMapper baseInfoMapper;
+    private final ReportFundInfoMapper fundInfoMapper;
+
+    public AbstractReportWriter(ReportBaseInfoMapper baseInfoMapper, ReportFundInfoMapper fundInfoMapper) {
+        this.baseInfoMapper = baseInfoMapper;
+        this.fundInfoMapper = fundInfoMapper;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void write(T reportData) {
+        if (reportData == null) {
+            this.logger.error("The report no result!");
+            return;
+        }
+        // 基本信息+基金信息保存
+        this.saveBaseInfo(reportData);
+        this.saveFundInfo(reportData);
+        // 其他信息保存
+        this.writeExtData(reportData);
+    }
+
+    private void saveBaseInfo(T reportData) {
+        ReportBaseInfoDTO baseInfo = reportData.getBaseInfo();
+        if (baseInfo != null) {
+            this.baseInfoMapper.insert(baseInfo.toEntity());
+        }
+    }
+
+    private void saveFundInfo(T reportData) {
+        ReportFundInfoDTO fundInfo = reportData.getFundInfo();
+        if (fundInfo != null) {
+            this.fundInfoMapper.insert(fundInfo.toEntity());
+        }
+    }
+
+    protected abstract void writeExtData(T reportData);
+}

+ 14 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/AnnuallyReportWriter.java

@@ -0,0 +1,14 @@
+//package com.smppw.modaq.infrastructure.components.report.writer;
+//
+//import com.simuwang.base.mapper.report.*;
+//import com.simuwang.base.pojo.dto.report.AnnuallyReportData;
+//import org.springframework.stereotype.Component;
+//
+//@Component(ReportWriterConstant.WRITER_ANNUALLY)
+//public class AnnuallyReportWriter extends QuarterlyReportWriter<AnnuallyReportData> {
+//    public AnnuallyReportWriter(ReportBaseInfoMapper baseInfoMapper, ReportFundInfoMapper fundInfoMapper,
+//                                ReportShareChangeMapper shareChangeMapper, ReportAssetAllocationMapper assetAllocationMapper,
+//                                ReportInvestmentIndustryMapper investmentIndustryMapper, ReportFinancialIndicatorMapper financialIndicatorMapper) {
+//        super(baseInfoMapper, fundInfoMapper, shareChangeMapper, assetAllocationMapper, investmentIndustryMapper, financialIndicatorMapper);
+//    }
+//}

+ 35 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/LetterReportWriter.java

@@ -0,0 +1,35 @@
+package com.smppw.modaq.application.components.report.writer;
+
+import com.smppw.modaq.domain.dto.report.LetterReportData;
+import com.smppw.modaq.domain.dto.report.ReportFundTransactionDTO;
+import com.smppw.modaq.domain.dto.report.ReportInvestorInfoDTO;
+import com.smppw.modaq.domain.mapper.ReportBaseInfoMapper;
+import com.smppw.modaq.domain.mapper.ReportFundInfoMapper;
+import com.smppw.modaq.domain.mapper.ReportFundTransactionMapper;
+import com.smppw.modaq.domain.mapper.ReportInvestorInfoMapper;
+import org.springframework.stereotype.Component;
+
+@Component(ReportWriterConstant.WRITER_LETTER)
+public class LetterReportWriter extends AbstractReportWriter<LetterReportData> {
+    private final ReportInvestorInfoMapper investorInfoMapper;
+    private final ReportFundTransactionMapper fundTransactionMapper;
+
+    public LetterReportWriter(ReportBaseInfoMapper baseInfoMapper, ReportFundInfoMapper fundInfoMapper,
+                              ReportInvestorInfoMapper investorInfoMapper, ReportFundTransactionMapper fundTransactionMapper) {
+        super(baseInfoMapper, fundInfoMapper);
+        this.investorInfoMapper = investorInfoMapper;
+        this.fundTransactionMapper = fundTransactionMapper;
+    }
+
+    @Override
+    protected void writeExtData(LetterReportData reportData) {
+        ReportInvestorInfoDTO investorInfo = reportData.getInvestorInfo();
+        if (investorInfo != null) {
+            this.investorInfoMapper.insert(investorInfo.toEntity());
+        }
+        ReportFundTransactionDTO fundTransaction = reportData.getFundTransaction();
+        if (fundTransaction != null) {
+            this.fundTransactionMapper.insert(fundTransaction.toEntity());
+        }
+    }
+}

+ 25 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/MonthlyReportWriter.java

@@ -0,0 +1,25 @@
+//package com.smppw.modaq.infrastructure.components.report.writer;
+//
+//import com.smppw.modaq.domain.mapper.ReportBaseInfoMapper;
+//import com.smppw.modaq.domain.mapper.ReportFundInfoMapper;
+//import com.smppw.modaq.infrastructure.dto.report.MonthlyReportData;
+//import org.springframework.stereotype.Component;
+//
+//@Component(ReportWriterConstant.WRITER_MONTHLY)
+//public class MonthlyReportWriter extends AbstractReportWriter<MonthlyReportData> {
+//
+//    public MonthlyReportWriter(ReportBaseInfoMapper baseInfoMapper,
+//                               ReportFundInfoMapper fundInfoMapper) {
+//        super(baseInfoMapper, fundInfoMapper);
+//    }
+//
+//    @Override
+//    protected void writeExtData(MonthlyReportData reportData) {
+////        List<ReportNetReportDTO> netReport = reportData.getNetReport();
+////        if (CollUtil.isNotEmpty(netReport)) {
+////            List<ReportNetReportDO> entityList = netReport.stream()
+////                    .map(ReportNetReportDTO::toEntity).collect(Collectors.toList());
+////            this.netReportMapper.insert(entityList);
+////        }
+//    }
+//}

+ 62 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/QuarterlyReportWriter.java

@@ -0,0 +1,62 @@
+//package com.smppw.modaq.infrastructure.components.report.writer;
+//
+//import cn.hutool.core.collection.CollUtil;
+//import com.simuwang.base.mapper.report.*;
+//import com.simuwang.base.pojo.dos.report.ReportAssetAllocationDO;
+//import com.simuwang.base.pojo.dos.report.ReportFinancialIndicatorsDO;
+//import com.simuwang.base.pojo.dos.report.ReportInvestmentIndustryDO;
+//import com.simuwang.base.pojo.dos.report.ReportShareChangeDO;
+//import com.simuwang.base.pojo.dto.report.*;
+//import org.springframework.stereotype.Component;
+//
+//import java.util.List;
+//import java.util.stream.Collectors;
+//
+//@Component(ReportWriterConstant.WRITER_QUARTERLY)
+//public class QuarterlyReportWriter<T extends QuarterlyReportData> extends AbstractReportWriter<T> {
+//    private final ReportShareChangeMapper shareChangeMapper;
+//    private final ReportAssetAllocationMapper assetAllocationMapper;
+//    private final ReportInvestmentIndustryMapper investmentIndustryMapper;
+//    private final ReportFinancialIndicatorMapper financialIndicatorMapper;
+//
+//    public QuarterlyReportWriter(ReportBaseInfoMapper baseInfoMapper, ReportFundInfoMapper fundInfoMapper,
+//                                 ReportShareChangeMapper shareChangeMapper, ReportAssetAllocationMapper assetAllocationMapper,
+//                                 ReportInvestmentIndustryMapper investmentIndustryMapper, ReportFinancialIndicatorMapper financialIndicatorMapper) {
+//        super(baseInfoMapper, fundInfoMapper);
+//        this.shareChangeMapper = shareChangeMapper;
+//        this.assetAllocationMapper = assetAllocationMapper;
+//        this.investmentIndustryMapper = investmentIndustryMapper;
+//        this.financialIndicatorMapper = financialIndicatorMapper;
+//    }
+//
+//    @Override
+//    protected void writeExtData(QuarterlyReportData reportData) {
+//        List<ReportShareChangeDTO> shareChange = reportData.getShareChange();
+//        if (CollUtil.isNotEmpty(shareChange)) {
+//            List<ReportShareChangeDO> entityList = shareChange.stream()
+//                    .map(ReportShareChangeDTO::toEntity).collect(Collectors.toList());
+//            this.shareChangeMapper.insert(entityList);
+//        }
+//
+//        List<ReportAssetAllocationDTO> assetAllocation = reportData.getAssetAllocation();
+//        if (CollUtil.isNotEmpty(assetAllocation)) {
+//            List<ReportAssetAllocationDO> entityList = assetAllocation.stream()
+//                    .map(ReportAssetAllocationDTO::toEntity).collect(Collectors.toList());
+//            this.assetAllocationMapper.insert(entityList);
+//        }
+//
+//        List<ReportFinancialIndicatorsDTO> financialIndicators = reportData.getFinancialIndicators();
+//        if (CollUtil.isNotEmpty(financialIndicators)) {
+//            List<ReportFinancialIndicatorsDO> entityList = financialIndicators.stream()
+//                    .map(ReportFinancialIndicatorsDTO::toEntity).collect(Collectors.toList());
+//            this.financialIndicatorMapper.insert(entityList);
+//        }
+//
+//        List<ReportInvestmentIndustryDTO> investmentIndustry = reportData.getInvestmentIndustry();
+//        if (CollUtil.isNotEmpty(investmentIndustry)) {
+//            List<ReportInvestmentIndustryDO> entityList = investmentIndustry.stream()
+//                    .map(ReportInvestmentIndustryDTO::toEntity).collect(Collectors.toList());
+//            this.investmentIndustryMapper.insert(entityList);
+//        }
+//    }
+//}

+ 13 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/ReportWriter.java

@@ -0,0 +1,13 @@
+package com.smppw.modaq.application.components.report.writer;
+
+
+import com.smppw.modaq.domain.dto.report.ReportData;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/29 14:06
+ * @description 报告存储保存的服务业务(可以扩展支持保存到本地缓存或文件)
+ */
+public interface ReportWriter<T extends ReportData> {
+    void write(T reportData);
+}

+ 23 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/ReportWriterConstant.java

@@ -0,0 +1,23 @@
+package com.smppw.modaq.application.components.report.writer;
+
+import cn.hutool.core.map.MapUtil;
+import com.smppw.modaq.common.enums.ReportType;
+
+import java.util.Map;
+
+public final class ReportWriterConstant {
+    public static final Map<ReportType, String> REPORT_TYPE_BEAN_MAP = MapUtil.newHashMap(8);
+
+    static final String WRITER_LETTER = "report-writer:letter";
+    static final String WRITER_MONTHLY = "report-writer:monthly";
+    static final String WRITER_QUARTERLY = "report-writer:quarterly";
+    static final String WRITER_ANNUALLY = "report-writer:annually";
+
+    static {
+        REPORT_TYPE_BEAN_MAP.put(ReportType.LETTER, WRITER_LETTER);
+
+        REPORT_TYPE_BEAN_MAP.put(ReportType.MONTHLY, WRITER_MONTHLY);
+        REPORT_TYPE_BEAN_MAP.put(ReportType.QUARTERLY, WRITER_QUARTERLY);
+        REPORT_TYPE_BEAN_MAP.put(ReportType.ANNUALLY, WRITER_ANNUALLY);
+    }
+}

+ 26 - 0
mo-daq/src/main/java/com/smppw/modaq/application/components/report/writer/ReportWriterFactory.java

@@ -0,0 +1,26 @@
+package com.smppw.modaq.application.components.report.writer;
+
+import cn.hutool.core.map.MapUtil;
+import com.smppw.modaq.common.enums.ReportType;
+import com.smppw.modaq.domain.dto.report.ReportData;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+@Component
+public class ReportWriterFactory {
+    private static final ReportWriter<?> DEFAULT = (ReportWriter<ReportData>) reportData -> {
+    };
+
+    private static final Map<String, ReportWriter<? extends ReportData>> REPORT_WRITER_MAP = MapUtil.newHashMap(8);
+
+    public ReportWriterFactory(Map<String, ReportWriter<? extends ReportData>> components) {
+        REPORT_WRITER_MAP.putAll(components);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends ReportData> ReportWriter<T> getInstance(ReportType reportType) {
+        String beanName = ReportWriterConstant.REPORT_TYPE_BEAN_MAP.get(reportType);
+        return (ReportWriter<T>) REPORT_WRITER_MAP.getOrDefault(beanName, DEFAULT);
+    }
+}

+ 40 - 0
mo-daq/src/main/java/com/smppw/modaq/application/service/EmailParseApiService.java

@@ -0,0 +1,40 @@
+package com.smppw.modaq.application.service;
+
+import com.smppw.modaq.domain.dto.MailboxInfoDTO;
+
+import java.util.Date;
+
+/**
+ * @author mozuwen
+ * @date 2024-09-12
+ * @description 邮件解析服务对外接口
+ */
+public interface EmailParseApiService {
+
+    void parseEmail(Date startDate, Date endDate);
+
+    /**
+     * 解析指定邮箱指定时间范围内的邮件
+     *
+     * @param mailboxInfoDTO 邮箱配置信息
+     * @param startDate      邮件起始日期(yyyy-MM-dd HH:mm:ss)
+     * @param endDate        邮件截止日期(yyyy-MM-dd HH:mm:ss, 为null,将解析邮件日期小于等于startDate的当天邮件)
+     */
+    void parseEmail(MailboxInfoDTO mailboxInfoDTO, Date startDate, Date endDate);
+
+    /**
+     * 重新解析指定邮件
+     *
+     * @param emailId 邮件id
+     */
+    void reparseEmail(Integer emailId);
+//
+//
+//    /**
+//     * 重新解析指定估值表文件
+//     *
+//     * @param fileIdList 文件id列表
+//     */
+//    void reparseFile(List<Integer> fileIdList);
+
+}

+ 277 - 0
mo-daq/src/main/java/com/smppw/modaq/application/service/EmailParseApiServiceImpl.java

@@ -0,0 +1,277 @@
+package com.smppw.modaq.application.service;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.smppw.modaq.application.util.EmailUtil;
+import com.smppw.modaq.common.conts.DateConst;
+import com.smppw.modaq.domain.dto.EmailContentInfoDTO;
+import com.smppw.modaq.domain.dto.EmailZipFileDTO;
+import com.smppw.modaq.domain.dto.MailboxInfoDTO;
+import com.smppw.modaq.domain.entity.EmailFileInfoDO;
+import com.smppw.modaq.domain.entity.EmailParseInfoDO;
+import com.smppw.modaq.domain.entity.MailboxInfoDO;
+import com.smppw.modaq.domain.mapper.EmailFileInfoMapper;
+import com.smppw.modaq.domain.mapper.EmailParseInfoMapper;
+import com.smppw.modaq.domain.mapper.MailboxInfoMapper;
+import com.smppw.modaq.domain.service.EmailParseService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author mozuwen
+ * @date 2024-09-12
+ * @description 邮件解析服务对外接口实现层
+ */
+@Service
+public class EmailParseApiServiceImpl implements EmailParseApiService {
+
+    private static final Logger log = LoggerFactory.getLogger(EmailParseApiServiceImpl.class);
+
+    private final MailboxInfoMapper mailboxInfoMapper;
+    private final EmailParseService emailParseService;
+    private final EmailParseInfoMapper emailParseInfoMapper;
+    private final EmailFileInfoMapper emailFileInfoMapper;
+    private final ThreadPoolTaskExecutor asyncExecutor;
+//    private final EmailTaskInfoMapper emailTaskInfoMapper;
+
+    public EmailParseApiServiceImpl(MailboxInfoMapper mailboxInfoMapper,
+                                    EmailParseService emailParseService,
+                                    EmailParseInfoMapper emailParseInfoMapper,
+                                    EmailFileInfoMapper emailFileInfoMapper,
+                                    @Qualifier("asyncExecutor") ThreadPoolTaskExecutor asyncExecutor) {
+        this.mailboxInfoMapper = mailboxInfoMapper;
+        this.emailParseService = emailParseService;
+        this.emailParseInfoMapper = emailParseInfoMapper;
+        this.emailFileInfoMapper = emailFileInfoMapper;
+        this.asyncExecutor = asyncExecutor;
+    }
+
+    @Override
+    public void parseEmail(Date startDate, Date endDate) {
+        List<MailboxInfoDO> mailboxList = this.mailboxInfoMapper.listMailboxInfo();
+        for (MailboxInfoDO mailbox : mailboxList) {
+            MailboxInfoDTO paramDTO = new MailboxInfoDTO();
+            paramDTO.setAccount(mailbox.getEmail());
+            paramDTO.setPassword(mailbox.getPassword());
+            paramDTO.setPort(mailbox.getPort());
+            paramDTO.setHost(mailbox.getServer());
+            paramDTO.setProtocol(mailbox.getProtocol());
+            this.parseEmail(paramDTO, startDate, endDate);
+        }
+    }
+
+    @Override
+    public void parseEmail(MailboxInfoDTO mailboxInfoDTO, Date startDate, Date endDate) {
+//        Integer userId = null;
+//        try{
+//            if(UserUtils.getPrincipal() == null){
+//                userId = 1;
+//            }else{
+//                userId = UserUtils.getLoginUser().getUserId();
+//            }
+//        }catch (Exception e){
+//            log.error(e.getMessage());
+//        }
+//        Integer finalUserId = userId;
+        asyncExecutor.execute(() -> {
+//                      EmailTaskInfoDO emailTaskInfoDO = startEmailTask(mailboxInfoDTO.getAccount(), 1, finalUserId);
+                    try {
+                        emailParseService.parseEmail(mailboxInfoDTO, startDate, endDate);
+                    } catch (Exception e) {
+                        log.error(e.getMessage(), e);
+//                          endEmailTask(emailTaskInfoDO.getId(),-1);
+//                        return;
+                    }
+//                      endEmailTask(emailTaskInfoDO.getId(),2);
+                }
+        );
+    }
+
+//    private void endEmailTask(Integer id,Integer taskStatus) {
+//        try{
+//            EmailTaskInfoDO emailTaskInfoDO = new EmailTaskInfoDO();
+//            emailTaskInfoDO.setId(id);
+//            emailTaskInfoDO.setTaskStatus(taskStatus);
+//            emailTaskInfoDO.setUpdateTime(DateUtils.getNowDate());
+//            emailTaskInfoDO.setEndTime(DateUtils.getNowDate());
+//            emailTaskInfoMapper.updateTaskStatusById(emailTaskInfoDO);
+//        }catch (Exception e){
+//            log.error(e.getMessage());
+//        }
+//    }
+//
+//    private EmailTaskInfoDO startEmailTask(String email,Integer taskStatus,Integer userId) {
+//        EmailTaskInfoDO  emailTaskInfoDO = new EmailTaskInfoDO();
+//        try{
+//            emailTaskInfoDO.setTaskName(TaskType.EMAIL_PARSE.getInfo());
+//            emailTaskInfoDO.setTaskType(TaskType.EMAIL_PARSE.getType());
+//            emailTaskInfoDO.setTaskStatus(taskStatus);
+//            emailTaskInfoDO.setStartTime(DateUtils.getNowDate());
+//            emailTaskInfoDO.setIsvalid(1);
+//            emailTaskInfoDO.setEmail(email);
+//            emailTaskInfoDO.setCreateTime(DateUtils.getNowDate());
+//            emailTaskInfoDO.setUpdateTime(DateUtils.getNowDate());
+//            emailTaskInfoDO.setCreatorId(userId);
+//            emailTaskInfoDO.setUpdaterId(userId);
+//            emailTaskInfoMapper.insert(emailTaskInfoDO);
+//        }catch (Exception e){
+//            log.error(e.getMessage());
+//        }
+//        return emailTaskInfoDO;
+//    }
+
+    @Override
+    public void reparseEmail(Integer emailId) {
+        // 查询邮件信息
+        EmailParseInfoDO emailParseInfoDO = emailParseInfoMapper.queryById(emailId);
+        if (emailParseInfoDO == null) {
+            log.info("邮件不存在 ->邮件id:{}", emailId);
+            return;
+        }
+        //解析成功的邮件不再解析
+        if (emailParseInfoDO.getParseStatus() == 1) {
+            log.info("邮件解析状态为成功,不再解析 ->邮件id:{}", emailId);
+            return;
+        }
+        List<EmailFileInfoDO> emailFileInfoDOList = emailFileInfoMapper.queryByEmailId(emailId);
+        if (CollUtil.isEmpty(emailFileInfoDOList)) {
+            log.info("该邮件不存在附件 -> 邮件id:{}", emailId);
+            return;
+        }
+//        // 邮件字段识别映射表
+//        Map<String, List<String>> emailFieldMap = emailParseService.getEmailFieldMapping();
+        // 邮件类型配置
+        Map<Integer, List<String>> emailTypeMap = emailParseService.getEmailType();
+
+        // 解析流程
+        List<EmailContentInfoDTO> emailContentInfoDTOList = buildEmailContentInfoDTO(emailId, emailParseInfoDO, emailFileInfoDOList, emailTypeMap);
+
+//        List<EmailFundNavDTO> emailFundNavDTOList = CollUtil.newArrayList();
+        Map<EmailContentInfoDTO, List<EmailZipFileDTO>> emailZipFileMap = MapUtil.newHashMap();
+        asyncExecutor.execute(() -> {
+            for (EmailContentInfoDTO emailContentInfoDTO : emailContentInfoDTOList) {
+                try {
+                    List<EmailZipFileDTO> emailZipFiles = emailParseService.parseZipEmail(emailContentInfoDTO);
+                    emailZipFileMap.put(emailContentInfoDTO, emailZipFiles);
+//                    emailFundNavDTOList.addAll(fundNavDTOList);
+                } catch (Exception e) {
+                    log.error("重新解析邮件失败,邮件id:{},堆栈信息:{}", emailId, ExceptionUtil.stacktraceToString(e));
+                }
+            }
+            // 保存相关信息 -> 邮件信息表,邮件文件表,邮件净值表,邮件规模表,基金净值表
+            emailParseService.saveRelatedTable(null, emailParseInfoDO.getEmail(), emailZipFileMap);
+        });
+    }
+//
+//    @Override
+//    public void reparseFile(List<Integer> fileIdList) {
+//        if (CollUtil.isEmpty(fileIdList)) {
+//            return;
+//        }
+//        List<EmailInfoDTO> emailParseInfoDOList = emailParseInfoMapper.queryValuationEmailByFileId(fileIdList);
+//        if (CollUtil.isEmpty(emailParseInfoDOList)) {
+//            return;
+//        }
+//        asyncExecutor.execute(() -> {
+//            Map<Integer, List<EmailInfoDTO>> emailIdFileMap = emailParseInfoDOList.stream().collect(Collectors.groupingBy(EmailInfoDTO::getId));
+//            for (Map.Entry<Integer, List<EmailInfoDTO>> entry : emailIdFileMap.entrySet()) {
+//                Integer emailId = entry.getKey();
+//                List<EmailInfoDTO> emailInfoDTOList = entry.getValue();
+//                String emailAddress = emailInfoDTOList.get(0).getEmail();
+//                List<EmailContentInfoDTO> emailContentInfoDTOList = emailInfoDTOList.stream().map(this::buildEmailContentInfoDTO).collect(Collectors.toList());
+//
+//                List<EmailFundNavDTO> emailFundNavDTOList = CollUtil.newArrayList();
+//                Map<EmailContentInfoDTO, List<EmailFundNavDTO>> fileNameNavMap = MapUtil.newHashMap();
+//                for (EmailContentInfoDTO emailContentInfoDTO : emailContentInfoDTOList) {
+//                    try {
+//                        log.info("开始重新解析文件 -> 文件id:{}", emailContentInfoDTO.getFileId());
+//                        List<EmailFundNavDTO> fundNavDTOList = emailParseService.parseEmail(emailContentInfoDTO, MapUtil.newHashMap());
+//                        fileNameNavMap.put(emailContentInfoDTO, fundNavDTOList);
+//                        emailFundNavDTOList.addAll(fundNavDTOList);
+//                    } catch (Exception e) {
+//                        log.error("重新解析文件失败,邮件id:{},文件id:{},堆栈信息:{}", emailId, emailContentInfoDTO.getFileId(), ExceptionUtil.stacktraceToString(e));
+//                    }
+//                }
+//                // 保存相关信息 -> 邮件信息表,邮件文件表,邮件净值表,邮件规模表,基金净值表
+//                emailParseService.saveRelatedTable(emailAddress, emailContentInfoDTOList, fileNameNavMap);
+//            }
+//            log.info("重新解析文件结束... -> 文件id:{}", fileIdList);
+//        });
+//    }
+
+//    private EmailContentInfoDTO buildEmailContentInfoDTO(EmailInfoDTO emailInfoDTO) {
+//        String emailDate = DateUtil.format(emailInfoDTO.getEmailDate(), DateConst.YYYY_MM_DD_HH_MM_SS);
+//        String parseDate = DateUtil.format(new Date(), DateConst.YYYY_MM_DD_HH_MM_SS);
+//        EmailContentInfoDTO contentInfoDTO = new EmailContentInfoDTO();
+//        contentInfoDTO.setEmailId(emailInfoDTO.getId());
+//        contentInfoDTO.setFileId(emailInfoDTO.getFileId());
+//        contentInfoDTO.setEmailAddress(emailInfoDTO.getEmail());
+//        contentInfoDTO.setEmailDate(emailDate);
+//        contentInfoDTO.setEmailTitle(emailInfoDTO.getEmailTitle());
+//        contentInfoDTO.setParseDate(parseDate);
+//        contentInfoDTO.setFileName(emailInfoDTO.getFileName());
+//        contentInfoDTO.setFilePath(emailInfoDTO.getFilePath());
+//        contentInfoDTO.setEmailType(emailInfoDTO.getEmailType());
+//        return contentInfoDTO;
+//    }
+
+    private List<EmailContentInfoDTO> buildEmailContentInfoDTO(Integer emailId,
+                                                               EmailParseInfoDO emailParseInfoDO,
+                                                               List<EmailFileInfoDO> emailFileInfoDOList,
+                                                               Map<Integer, List<String>> emailTypeMap) {
+        List<EmailContentInfoDTO> emailContentInfoDTOList = CollUtil.newArrayList();
+        String emailDate = DateUtil.format(emailParseInfoDO.getEmailDate(), DateConst.YYYY_MM_DD_HH_MM_SS);
+        String parseDate = DateUtil.format(new Date(), DateConst.YYYY_MM_DD_HH_MM_SS);
+        for (EmailFileInfoDO fileInfoDO : emailFileInfoDOList) {
+            EmailContentInfoDTO contentInfoDTO = new EmailContentInfoDTO();
+            contentInfoDTO.setEmailId(emailId);
+            contentInfoDTO.setFileId(fileInfoDO.getId());
+            contentInfoDTO.setSenderEmail(emailParseInfoDO.getSenderEmail());
+            contentInfoDTO.setEmailAddress(emailParseInfoDO.getEmail());
+            contentInfoDTO.setEmailDate(emailDate);
+            contentInfoDTO.setEmailTitle(emailParseInfoDO.getEmailTitle());
+            contentInfoDTO.setParseDate(parseDate);
+            contentInfoDTO.setFileName(fileInfoDO.getFileName());
+            contentInfoDTO.setFilePath(fileInfoDO.getFilePath());
+            Integer emailType = EmailUtil.getEmailTypeBySubject(emailParseInfoDO.getEmailTitle(), emailTypeMap);
+            contentInfoDTO.setEmailType(emailType);
+            String emailContent = readHtmlFileContent(fileInfoDO.getFilePath());
+            contentInfoDTO.setEmailContent(emailContent);
+            contentInfoDTO.setAiFileId(fileInfoDO.getAiFileId());
+            emailContentInfoDTOList.add(contentInfoDTO);
+        }
+        return emailContentInfoDTOList;
+    }
+
+    public static String readHtmlFileContent(String filePath) {
+        if (StrUtil.isNotBlank(filePath) && filePath.endsWith("html")) {
+            StringBuilder content = new StringBuilder();
+            try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    // 追加每一行到StringBuilder中
+                    content.append(line).append("\n");
+                }
+            } catch (IOException e) {
+                System.err.println("Error reading the file: " + e.getMessage());
+                return null;
+            }
+            return content.toString();
+        }
+        return null;
+    }
+
+}

+ 25 - 0
mo-daq/src/main/java/com/smppw/modaq/application/task/ParseSchedulerTask.java

@@ -0,0 +1,25 @@
+package com.smppw.modaq.application.task;
+
+import cn.hutool.core.date.DateUtil;
+import com.smppw.modaq.application.service.EmailParseApiService;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+@Component
+@EnableScheduling
+public class ParseSchedulerTask {
+    private final EmailParseApiService emailParseApiService;
+
+    public ParseSchedulerTask(EmailParseApiService emailParseApiService) {
+        this.emailParseApiService = emailParseApiService;
+    }
+
+    @Scheduled(cron = "0 0 4 * * ?") // 每天早上4点执行
+    public void run() {
+        Date preDay = DateUtil.offsetDay(new Date(), -1);
+        this.emailParseApiService.parseEmail(preDay, new Date());
+    }
+}

+ 367 - 0
mo-daq/src/main/java/com/smppw/modaq/application/util/EmailUtil.java

@@ -0,0 +1,367 @@
+package com.smppw.modaq.application.util;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.mail.JakartaUserPassAuthenticator;
+import com.smppw.modaq.common.conts.DateConst;
+import com.smppw.modaq.common.conts.EmailTypeConst;
+import com.smppw.modaq.domain.dto.EmailContentInfoDTO;
+import com.smppw.modaq.domain.dto.MailboxInfoDTO;
+import com.smppw.modaq.infrastructure.util.ExcelUtil;
+import com.smppw.modaq.infrastructure.util.FileUtil;
+import com.sun.mail.imap.IMAPStore;
+import jakarta.activation.DataHandler;
+import jakarta.mail.*;
+import jakarta.mail.internet.*;
+import jakarta.mail.util.ByteArrayDataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.*;
+
+/**
+ * @author mozuwen
+ * @date 2024-09-04
+ * @description 邮件解析工具
+ */
+public class EmailUtil {
+
+    private static final Logger logger = LoggerFactory.getLogger(EmailUtil.class);
+
+    private static final String POP3 = "pop3";
+    private static final String IMAP = "imap";
+
+    /**
+     * 采集邮件(多消息体)信息
+     *
+     * @param message      邮件
+     * @param emailAddress 邮箱地址
+     * @param path         存储路径
+     * @return 从邮箱采集到的信息
+     * @throws Exception 异常信息
+     */
+    public static List<EmailContentInfoDTO> collectMimeMultipart(Message message, String emailAddress, String path) throws Exception {
+        List<EmailContentInfoDTO> emailContentInfoDTOList = CollUtil.newArrayList();
+        String emailTitle = message.getSubject();
+        String emailDate = DateUtil.format(message.getSentDate(), DateConst.YYYYMMDDHHMMSS24);
+        String emailDateStr = DateUtil.format(message.getSentDate(), DateConst.YYYYMMDD);
+        String filePath = path + "/" + emailAddress + "/" + emailDateStr + "/";
+
+        MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
+        int length = mimeMultipart.getCount();
+        // 遍历邮件消息体 (我这里不要html正文)
+        for (int i = 0; i < length; i++) {
+            EmailContentInfoDTO emailContentInfoDTO = new EmailContentInfoDTO();
+            MimeBodyPart part = (MimeBodyPart) mimeMultipart.getBodyPart(i);
+//            Object partContent = part.getContent();
+            String contentClass = part.getContent().getClass().getSimpleName();
+            // 1.邮件正文
+            if ("String".equals(contentClass)) {
+//                // 文件名 = 邮件主题 + 邮件日期
+//                String fileName = emailTitle + "_" + emailDate + ".html";
+//                String content = partContent.toString();
+//                emailContentInfoDTO = collectTextPart(part, content, filePath, fileName);
+            } else if ("BASE64DecoderStream".equals(contentClass)) {
+                if (StrUtil.isNotBlank(part.getFileName())) {
+                    String fileName = MimeUtility.decodeText(part.getFileName());
+                    if (!isSupportedFileType(fileName)) {
+                        continue;
+                    }
+                    emailContentInfoDTO.setFileName(fileName);
+
+                    String realPath = filePath + emailDate + fileName;
+
+                    File saveFile = cn.hutool.core.io.FileUtil.file(realPath);
+                    if (!saveFile.exists()) {
+                        if (!saveFile.getParentFile().exists()) {
+                            saveFile.getParentFile().mkdirs();
+                        }
+                        FileUtil.saveFile(saveFile, part);
+                    } else {
+                        cn.hutool.core.io.FileUtil.del(saveFile);
+                        FileUtil.saveFile(saveFile, part);
+                    }
+                    emailContentInfoDTO.setFilePath(realPath);
+                }
+            } else if ("MimeMultipart".equals(contentClass)) {
+//                MimeMultipart contentPart = (MimeMultipart) partContent;
+//                int length2 = contentPart.getCount();
+//                for (int i2 = 0; i2 < length2; i2++) {
+//                    part = (MimeBodyPart) contentPart.getBodyPart(i2);
+//                    partContent = part.getContent();
+//                    contentClass = partContent.getClass().getSimpleName();
+//                    if ("String".equals(contentClass)) {
+//                        // 文件名 = 邮件主题 + 邮件日期
+//                        String fileName = emailTitle + "_" + emailDate + ".html";
+//                        String content = partContent.toString();
+//                        emailContentInfoDTO = collectTextPart(part, content, filePath, fileName);
+//                    }
+//                }
+            }
+            String filepath = emailContentInfoDTO.getFilePath();
+            if (emailContentInfoDTO.getEmailContent() == null && filepath == null) {
+                continue;
+            }
+            emailContentInfoDTO.setEmailAddress(emailAddress);
+            emailContentInfoDTO.setEmailTitle(emailTitle);
+            emailContentInfoDTO.setEmailDate(DateUtil.format(message.getSentDate(), DateConst.YYYY_MM_DD_HH_MM_SS));
+            emailContentInfoDTOList.add(emailContentInfoDTO);
+        }
+
+        return emailContentInfoDTOList;
+    }
+
+    private static List<EmailContentInfoDTO> zipFile(String filepath) {
+        return null;
+    }
+
+    private static boolean isSupportedFileType(String fileName) {
+        if (StrUtil.isBlank(fileName)) {
+            return false;
+        }
+        return ExcelUtil.isZip(fileName) || ExcelUtil.isExcel(fileName) || ExcelUtil.isPdf(fileName) || ExcelUtil.isHTML(fileName) || ExcelUtil.isRAR(fileName);
+    }
+
+    /**
+     * 根据日期过滤邮件
+     *
+     * @param messages  采集到的邮件
+     * @param startDate 邮件起始日期
+     * @param endDate   邮件截止日期
+     * @return 符合日期的邮件
+     */
+    public static List<Message> filterMessage(Message[] messages, Date startDate, Date endDate) {
+        long startTime = System.currentTimeMillis();
+        List<Message> messageList = CollUtil.newArrayList();
+        if (messages == null) {
+            return messageList;
+        }
+        for (Message message : messages) {
+            try {
+                if (message.getSentDate().compareTo(startDate) >= 0 && message.getSentDate().compareTo(endDate) <= 0) {
+                    messageList.add(message);
+                }
+            } catch (MessagingException e) {
+                throw new RuntimeException(e);
+            }
+        }
+        logger.info("根据日期过滤邮件耗时 -> {}ms", (System.currentTimeMillis() - startTime));
+        return messageList;
+    }
+
+    /**
+     * 采集邮件正文
+     *
+     * @param part        邮件消息体
+     * @param partContent 邮件消息内筒
+     * @param filePath    文件路径
+     * @param fileName    文件名
+     * @return 采集到邮件正文(html格式包含table标签)
+     */
+    public static EmailContentInfoDTO collectTextPart(MimeBodyPart part, String partContent, String filePath, String fileName) {
+        EmailContentInfoDTO emailContentInfoDTO = new EmailContentInfoDTO();
+        try {
+            if ((part.getContentType().contains("text/html") || part.getContentType().contains("TEXT/HTML"))) {
+                emailContentInfoDTO.setEmailContent(partContent.toString());
+                String savePath = filePath + fileName;
+                File saveFile = new File(savePath);
+                if (!saveFile.exists()) {
+                    if (!saveFile.getParentFile().exists()) {
+                        saveFile.getParentFile().mkdirs();
+                        saveFile.getParentFile().setExecutable(true);
+                    }
+                }
+                //获取邮件编码
+                String contentType = part.getContentType();
+                String html = partContent.toString();
+                try {
+                    if (contentType.indexOf("charset=") != -1) {
+                        contentType = contentType.substring(contentType.indexOf("charset=") + 8, contentType.length()).replaceAll("\"", "");
+                        html = html.replace("charset=" + contentType.toLowerCase(), "charset=UTF-8");
+                        html = html.replace("charset=" + contentType.toUpperCase(), "charset=UTF-8");
+                    }
+                    if (savePath.contains(":")) {
+                        savePath = savePath.replaceAll(":", "");
+                    }
+                    cn.hutool.core.io.FileUtil.writeUtf8String(html, new File(savePath));
+                } catch (Exception e) {
+                    logger.error(e.getMessage(), e);
+                }
+                emailContentInfoDTO.setFileName(fileName);
+                emailContentInfoDTO.setFilePath(savePath);
+            } else {
+                try {
+                    if (part.getFileName() == null) {
+                        return emailContentInfoDTO;
+                    }
+                    String fileName1 = MimeUtility.decodeText(part.getFileName());
+                    if (!isSupportedFileType(fileName1)) {
+                        return emailContentInfoDTO;
+                    }
+                    emailContentInfoDTO.setFileName(fileName1);
+                    String realPath = filePath + fileName1;
+                    File saveFile = new File(realPath);
+                    if (!saveFile.exists()) {
+                        if (!saveFile.getParentFile().exists()) {
+                            saveFile.getParentFile().mkdirs();
+                        }
+                        FileUtil.saveFile(saveFile, part);
+                    } else {
+                        cn.hutool.core.io.FileUtil.del(saveFile);
+                        FileUtil.saveFile(saveFile, part);
+                    }
+                    emailContentInfoDTO.setFilePath(realPath);
+                } catch (Exception e) {
+                    return emailContentInfoDTO;
+                }
+            }
+        } catch (MessagingException e) {
+            logger.info("邮件正文采集失败 -> 文件名:{}, 报错堆栈:{}", fileName, ExceptionUtil.stacktraceToString(e));
+            return emailContentInfoDTO;
+        }
+        return emailContentInfoDTO;
+    }
+
+    /**
+     * 判断邮件是否符合解析条件
+     *
+     * @param subject      邮件主题
+     * @param emailTypeMap 邮件类型识别规则映射表
+     * @return 邮件类型:1-净值,2-估值表,3-定期报告 -> 兜底为净值类型
+     */
+    public static Integer getEmailTypeBySubject(String subject, Map<Integer, List<String>> emailTypeMap) {
+        if (MapUtil.isEmpty(emailTypeMap) || StrUtil.isBlank(subject)) {
+            return EmailTypeConst.NAV_EMAIL_TYPE;
+        }
+        for (Map.Entry<Integer, List<String>> emailTypeEntry : emailTypeMap.entrySet()) {
+            for (String field : emailTypeEntry.getValue()) {
+                if (subject.contains(field)) {
+                    return emailTypeEntry.getKey();
+                }
+            }
+        }
+        return EmailTypeConst.NAV_EMAIL_TYPE;
+    }
+
+    public static Store getStoreNew(MailboxInfoDTO mailboxInfoDTO) {
+        // 配置连接邮件服务器参数
+        Properties props = getMailProps(mailboxInfoDTO);
+        // 创建Session实例对象
+        Session session = Session.getInstance(props, new JakartaUserPassAuthenticator(mailboxInfoDTO.getAccount(), mailboxInfoDTO.getPassword()));
+        Store store;
+        try {
+            String protocol = mailboxInfoDTO.getProtocol().equals(IMAP) ? "imaps" : "pop3";
+            if (mailboxInfoDTO.getProtocol().contains(IMAP)) {
+                IMAPStore imapStore = (IMAPStore) session.getStore(protocol);
+                imapStore.connect(mailboxInfoDTO.getHost(), mailboxInfoDTO.getAccount(), mailboxInfoDTO.getPassword());
+                // 网易邮箱需要带上身份标识,详情请看:https://www.hmail163.com/content/?404.html
+                Map<String, String> clientParams = new HashMap<>(2);
+                clientParams.put("name", "my-imap");
+                clientParams.put("version", "1.0");
+                imapStore.id(clientParams);
+                return imapStore;
+            } else {
+                store = session.getStore(protocol);
+                store.connect(mailboxInfoDTO.getHost(), mailboxInfoDTO.getAccount(), mailboxInfoDTO.getPassword());
+                return store;
+            }
+        } catch (Exception e) {
+            logger.error("邮箱信息:{},服务器参数:{}", mailboxInfoDTO, props);
+            logger.error("连接邮箱报错堆栈信息:{}", ExceptionUtil.stacktraceToString(e));
+            return null;
+        }
+    }
+
+    public static Properties getMailProps(MailboxInfoDTO mailboxInfoDTO) {
+        Properties props = new Properties();
+        if (mailboxInfoDTO.getProtocol().equalsIgnoreCase(POP3)) {
+            props.put("mail.pop3.host", mailboxInfoDTO.getHost());
+            props.put("mail.pop3.user", mailboxInfoDTO.getAccount());
+            props.put("mail.pop3.socketFactory", mailboxInfoDTO.getPort());
+            props.put("mail.pop3.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
+            props.put("mail.pop3.port", mailboxInfoDTO.getPort());
+            props.put("mail.store.protocol", mailboxInfoDTO.getProtocol());
+        }
+        if (mailboxInfoDTO.getProtocol().equalsIgnoreCase(IMAP)) {
+            props.put("mail.store.protocol", "imaps");
+            props.put("mail.imap.host", mailboxInfoDTO.getHost());
+            props.put("mail.imap.port", mailboxInfoDTO.getPort());
+            props.put("mail.imaps.ssl.enable", "true");
+            props.put("mail.imaps.ssl.trust", "*");
+            props.put("mail.imap.auth", "true");
+            props.put("mail.imap.starttls.enable", "true");
+            props.put("mail.imap.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
+            props.put("mail.imap.socketFactory.fallback", "false");
+        }
+        return props;
+    }
+
+    public static void senEmail(MailboxInfoDTO mailboxInfoDTO, String emails, File file, String htmlText, String host, String emailTitle) throws Exception {
+        logger.info("send email begin .........");
+        // 根据Session 构建邮件信息
+        MimeMessage message = new MimeMessage(getSession(mailboxInfoDTO));
+        // 创建邮件发送者地址
+        Address from = new InternetAddress(mailboxInfoDTO.getAccount() + host);
+        String[] emailArr = emails.split(";");
+        Address[] toArr = new Address[emailArr.length];
+        for (int idx = 0; idx < emailArr.length; idx++) {
+            if (StrUtil.isNotBlank(emailArr[idx])) {
+                Address to = new InternetAddress(emailArr[idx]);
+                toArr[idx] = to;
+            }
+        }
+        message.setFrom(from);
+        message.setRecipients(Message.RecipientType.TO, toArr);
+        // 邮件主题
+        message.setSubject(emailTitle);
+        // 邮件容器
+        MimeMultipart mimeMultiPart = new MimeMultipart();
+        // 设置HTML
+        BodyPart bodyPart = new MimeBodyPart();
+        logger.info("组装 htmlText.........");
+        // 邮件内容
+        bodyPart.setContent(htmlText, "text/html;charset=utf-8");
+        //设置附件
+        BodyPart filePart = new MimeBodyPart();
+        filePart.setFileName(file.getName());
+        filePart.setDataHandler(
+                new DataHandler(
+                        new ByteArrayDataSource(
+                                Files.readAllBytes(Paths.get(file.getAbsolutePath())), "application/octet-stream")));
+        mimeMultiPart.addBodyPart(bodyPart);
+        mimeMultiPart.addBodyPart(filePart);
+        message.setContent(mimeMultiPart);
+        message.setSentDate(new Date());
+        // 保存邮件
+        message.saveChanges();
+        // 发送邮件
+        Transport.send(message);
+    }
+
+    public static Session getSession(MailboxInfoDTO mailboxInfoDTO) {
+        try {
+            Properties properties = new Properties();
+            properties.put("mail.smtp.host", mailboxInfoDTO.getHost());
+            properties.put("mail.smtp.auth", true);
+            properties.put("mail.smtp.port", mailboxInfoDTO.getPort());
+            properties.put("mail.smtp.ssl.enable", true);
+            final String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory";
+            properties.setProperty("mail.smtp.socketFactory.class", SSL_FACTORY);
+            properties.setProperty("mail.smtp.socketFactory.fallback", "false");
+            properties.setProperty("mail.smtp.socketFactory.port", mailboxInfoDTO.getPort());
+            // 根据邮件的会话属性构造一个发送邮件的Session,
+            JakartaUserPassAuthenticator authenticator = new JakartaUserPassAuthenticator(mailboxInfoDTO.getAccount(), mailboxInfoDTO.getPassword());
+            Session session = Session.getInstance(properties, authenticator);
+            return session;
+        } catch (Exception e) {
+            logger.error("getSession : " + e.getMessage());
+        }
+        return null;
+    }
+}

+ 7 - 0
mo-daq/src/main/java/com/smppw/modaq/common/conts/Constants.java

@@ -0,0 +1,7 @@
+package com.smppw.modaq.common.conts;
+
+public class Constants {
+    public static final long DEFAULT_SERIAL_ID = 999L;
+
+    public static final String WATERMARK_REPLACE = "+_+" + System.lineSeparator();
+}

+ 12 - 0
mo-daq/src/main/java/com/smppw/modaq/common/conts/DateConst.java

@@ -0,0 +1,12 @@
+package com.smppw.modaq.common.conts;
+
+public class DateConst {
+
+    public final static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
+    public final static String YYYY_MM_DD_HH_MM = "yyyy-MM-dd HH:mm";
+    public final static String YYYY_MM_DD = "yyyy-MM-dd";
+    public final static String YYYYMMDD = "yyyyMMdd";
+    public final static String YYYYMMDDHHMMSS = "yyyyMMddHHmmssSSS";
+    public final static String YYYYMMDDHHMMSS24 = "yyyyMMddHHmmss";
+
+}

+ 15 - 0
mo-daq/src/main/java/com/smppw/modaq/common/conts/EmailParseStatusConst.java

@@ -0,0 +1,15 @@
+package com.smppw.modaq.common.conts;
+
+public class EmailParseStatusConst {
+
+    /**
+     * 成功
+     */
+    public final static Integer SUCCESS = 1;
+
+    /**
+     * 失败
+     */
+    public final static Integer FAIL = 2;
+
+}

+ 25 - 0
mo-daq/src/main/java/com/smppw/modaq/common/conts/EmailTypeConst.java

@@ -0,0 +1,25 @@
+package com.smppw.modaq.common.conts;
+
+public class EmailTypeConst {
+
+    /**
+     * 净值邮件类型
+     */
+    public final static Integer NAV_EMAIL_TYPE = 1;
+
+    /**
+     * 估值表邮件类型
+     */
+    public final static Integer VALUATION_EMAIL_TYPE = 2;
+
+    /**
+     * 定期报告邮件类型
+     */
+    public final static Integer REPORT_EMAIL_TYPE = 3;
+
+    /**
+     * 确认函
+     */
+    public final static Integer REPORT_LETTER_EMAIL_TYPE = 4;
+
+}

+ 36 - 0
mo-daq/src/main/java/com/smppw/modaq/common/enums/ReportParseStatus.java

@@ -0,0 +1,36 @@
+package com.smppw.modaq.common.enums;
+
+public enum ReportParseStatus implements StatusCode {
+    PARSE_FAIL(21000, "定期报告解析错误:{}"),
+    NOT_A_REPORT(21001, "[{}]不是定期报告"),
+    REPORT_IS_SCAN(21002, "报告[{}]为扫描件"),
+    NO_SUPPORT_TEMPLATE(21003, "报告[{}]是不支持的文件格式"),
+    NOT_A_FIXED_FORMAT(21004, "报告[{}]不是基协统一格式"),
+
+    PARSE_FUND_INFO_FAIL(21010, "报告[{}]没有解析到基金基本信息"),
+    PARSE_NAV_INFO_FAIL(21011, "报告[{}]没有解析到基金净值信息"),
+    PARSE_FINANCIAL_INFO_FAIL(21012, "报告[{}]没有解析到基金财务指标信息"),
+    PARSE_INDUSTRY_INFO_FAIL(21013, "报告[{}]没有解析到基金行业配置信息"),
+    PARSE_ASSET_INFO_FAIL(21014, "报告[{}]没有解析到基金资产配置信息"),
+    PARSE_SHARE_INFO_FAIL(21015, "报告[{}]没有解析到基金份额变动信息"),
+
+    PARSE_RULE_NO_FUND(21020, "未设置报告解析规则"),
+    ;
+    private final int code;
+    private final String msg;
+
+    ReportParseStatus(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    @Override
+    public int getCode() {
+        return this.code;
+    }
+
+    @Override
+    public String getMsg() {
+        return this.msg;
+    }
+}

+ 30 - 0
mo-daq/src/main/java/com/smppw/modaq/common/enums/ReportParserFileType.java

@@ -0,0 +1,30 @@
+package com.smppw.modaq.common.enums;
+
+import java.util.Arrays;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/29 10:57
+ * @description 解析文件格式类型,支持调用python接口解析
+ */
+public enum ReportParserFileType {
+    PDF("pdf"),
+    WORD("docx,doc"),
+    EXCEL("xlsx,xls"),
+    PYTHON("python");
+
+    private final String suffix;
+
+    ReportParserFileType(String suffix) {
+        this.suffix = suffix;
+    }
+
+    public static ReportParserFileType getBySuffix(String suffix) {
+        return Arrays.stream(ReportParserFileType.values())
+                .filter(e -> e.getSuffix().contains(suffix)).findFirst().orElse(null);
+    }
+
+    public String getSuffix() {
+        return suffix;
+    }
+}

+ 21 - 0
mo-daq/src/main/java/com/smppw/modaq/common/enums/ReportType.java

@@ -0,0 +1,21 @@
+package com.smppw.modaq.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum ReportType {
+    LETTER(-1, "交易流水确认函", new String[]{"确认单", "确认函", "确认"}),
+    MONTHLY(0, "月", new String[]{"月", "月度", "月报"}),
+    QUARTERLY(1, "季", new String[]{"季", "季度", "季报"}),
+    ANNUALLY(2, "年", new String[]{"年度", "年报"});
+
+    private final int type;
+    private final String label;
+    private final String[] patterns;
+
+    ReportType(int type, String label, String[] patterns) {
+        this.type = type;
+        this.label = label;
+        this.patterns = patterns;
+    }
+}

+ 33 - 0
mo-daq/src/main/java/com/smppw/modaq/common/enums/ResultCode.java

@@ -0,0 +1,33 @@
+package com.smppw.modaq.common.enums;
+
+import lombok.Generated;
+
+/**
+ * FileName: ResultCode
+ * Author:   chenjianhua
+ * Date:     2024/9/9 15:00
+ * Description: ${DESCRIPTION}
+ */
+public enum ResultCode {
+    CONNECT_SUCCESS(20000, "链接成功"),
+    CONNECT_ERROR(20005, "连接失败,请检查账号及协议相关信息"),
+    SEND_SUCCESS(200, "邮件发送成功");
+
+    private final int code;
+    private final String msg;
+
+    private ResultCode(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    @Generated
+    public int getCode() {
+        return this.code;
+    }
+
+    @Generated
+    public String getMsg() {
+        return this.msg;
+    }
+}

+ 7 - 0
mo-daq/src/main/java/com/smppw/modaq/common/enums/StatusCode.java

@@ -0,0 +1,7 @@
+package com.smppw.modaq.common.enums;
+
+public interface StatusCode {
+    int getCode();
+
+    String getMsg();
+}

+ 36 - 0
mo-daq/src/main/java/com/smppw/modaq/common/exception/ReportParseException.java

@@ -0,0 +1,36 @@
+package com.smppw.modaq.common.exception;
+
+import cn.hutool.core.util.StrUtil;
+import com.smppw.modaq.common.enums.StatusCode;
+
+/**
+ * @author wangzaijun
+ * @date 2024/10/11 14:10
+ * @description 报告解析的异常
+ */
+public class ReportParseException extends RuntimeException {
+    private final Integer code;
+    private final String msg;
+
+    public ReportParseException(StatusCode statusCode) {
+        this(statusCode.getCode(), statusCode.getMsg());
+    }
+
+    public ReportParseException(Integer code, String msg) {
+        super(msg);
+        this.code = code;
+        this.msg = msg;
+    }
+
+    public ReportParseException(StatusCode statusCode, Object... msgs) {
+        this(statusCode.getCode(), StrUtil.format(statusCode.getMsg(), msgs));
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+}

+ 17 - 0
mo-daq/src/main/java/com/smppw/modaq/common/support/DTO.java

@@ -0,0 +1,17 @@
+package com.smppw.modaq.common.support;
+
+
+import com.smppw.modaq.common.conts.Constants;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/13 13:46
+ * @description 抽象的接口请求参数父类
+ */
+public abstract class DTO implements Serializable {
+    @Serial
+    private static final long serialVersionUID = Constants.DEFAULT_SERIAL_ID;
+}

+ 41 - 0
mo-daq/src/main/java/com/smppw/modaq/common/support/dos/BaseEntity.java

@@ -0,0 +1,41 @@
+package com.smppw.modaq.common.support.dos;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.smppw.modaq.common.conts.Constants;
+import com.smppw.modaq.common.support.vo.BaseVO;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author wangzaijun
+ * @date 2023/11/7 17:23
+ * @description base数据模型
+ */
+@Setter
+@Getter
+public abstract class BaseEntity<VO extends BaseVO> implements Serializable {
+    @Serial
+    private static final long serialVersionUID = Constants.DEFAULT_SERIAL_ID;
+    /**
+     * 创建人
+     */
+    @TableField(value = "creatorid", fill = FieldFill.INSERT)
+    private Integer creatorId;
+    /**
+     * 创建时间
+     */
+    @TableField(value = "createtime", fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /**
+     * 当前数据库对象转vo对象,尽量用属性复制的方法,少用反射
+     *
+     * @return /
+     */
+    public abstract VO toVo();
+}

+ 36 - 0
mo-daq/src/main/java/com/smppw/modaq/common/support/dos/DataEntity.java

@@ -0,0 +1,36 @@
+package com.smppw.modaq.common.support.dos;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.smppw.modaq.common.support.vo.BaseVO;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * @author wangzaijun
+ * @date 2023/11/7 17:23
+ * @description base数据模型
+ */
+@Setter
+@Getter
+public abstract class DataEntity<VO extends BaseVO> extends BaseEntity<VO> {
+    /**
+     * 更新人
+     */
+    @TableField(value = "updaterid", fill = FieldFill.INSERT_UPDATE)
+    private Integer updaterId;
+    /**
+     * 更新时间
+     */
+    @TableField(value = "updatetime", fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+    /**
+     * 是否有效标识
+     */
+    @TableLogic(value = "1", delval = "0")
+    @TableField(value = "isvalid", fill = FieldFill.INSERT)
+    private Integer valid;
+}

+ 41 - 0
mo-daq/src/main/java/com/smppw/modaq/common/support/dos/OnlyIdNameDO.java

@@ -0,0 +1,41 @@
+package com.smppw.modaq.common.support.dos;
+
+import com.smppw.modaq.common.conts.Constants;
+import com.smppw.modaq.common.support.vo.OnlyIdNameVO;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/14 9:32
+ * @description 仅包含id和名称的对象
+ */
+@Setter
+@Getter
+public class OnlyIdNameDO implements Serializable {
+    @Serial
+    private static final long serialVersionUID = Constants.DEFAULT_SERIAL_ID;
+    /**
+     * 唯一标识
+     */
+    private Integer id;
+    /**
+     * 对应的名称
+     */
+    private String name;
+
+    /**
+     * 提供把数据库对象转vo对象的方法
+     *
+     * @return /
+     */
+    public OnlyIdNameVO toVo() {
+        OnlyIdNameVO vo = new OnlyIdNameVO();
+        vo.setId(id);
+        vo.setName(name);
+        return vo;
+    }
+}

+ 10 - 0
mo-daq/src/main/java/com/smppw/modaq/common/support/vo/BaseMultiJoinVO.java

@@ -0,0 +1,10 @@
+package com.smppw.modaq.common.support.vo;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/13 13:55
+ * @description 多表关联的返回对象抽象
+ */
+public abstract class BaseMultiJoinVO extends BaseVO {
+
+}

+ 18 - 0
mo-daq/src/main/java/com/smppw/modaq/common/support/vo/BaseVO.java

@@ -0,0 +1,18 @@
+package com.smppw.modaq.common.support.vo;
+
+
+import com.smppw.modaq.common.support.DTO;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/13 13:55
+ * @description 抽象的返回对象
+ */
+public abstract class BaseVO extends DTO {
+    /**
+     * 唯一id
+     *
+     * @return /
+     */
+    public abstract Integer getId();
+}

+ 28 - 0
mo-daq/src/main/java/com/smppw/modaq/common/support/vo/OnlyIdNameVO.java

@@ -0,0 +1,28 @@
+package com.smppw.modaq.common.support.vo;
+
+import com.smppw.modaq.common.conts.Constants;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/14 9:36
+ * @description 仅包含id和名称的对象
+ */
+@Setter
+@Getter
+public class OnlyIdNameVO implements Serializable {
+    @Serial
+    private static final long serialVersionUID = Constants.DEFAULT_SERIAL_ID;
+    /**
+     * 唯一标识
+     */
+    private Integer id;
+    /**
+     * 对应的名称
+     */
+    private String name;
+}

+ 71 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/EmailContentInfoDTO.java

@@ -0,0 +1,71 @@
+package com.smppw.modaq.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class EmailContentInfoDTO implements Serializable {
+
+    private static final long serialVersionUID = 202104140906313753L;
+
+    /**
+     * 邮件id(重新解析邮件功能)
+     */
+    private Integer emailId;
+    /**
+     * 邮箱地址
+     */
+    private String emailAddress;
+
+    /**
+     * 邮件主题
+     */
+    private String emailTitle;
+
+    /**
+     * 邮件日期:yyyyMMdd HH:mm:ss
+     */
+    private String emailDate;
+
+    /**
+     * 解析时间
+     */
+    private String parseDate;
+
+    /**
+     * 附件名称
+     */
+    private String fileName;
+
+    /**
+     * 附件地址
+     */
+    private String filePath;
+
+    /**
+     * 文件类型:1-净值文件,2-估值表文件,3-定期报告
+     */
+    private Integer emailType;
+
+    /**
+     * 邮件内容
+     */
+    private String emailContent;
+
+    /**
+     * 邮件发送人
+     */
+    private String senderEmail;
+
+    /**
+     * 文件id(重新解析邮件功能)
+     */
+    private Integer fileId;
+
+    /**
+     * ai解析时的文件id(重新解析邮件时用这个可以不用重复上传)
+     */
+    private String aiFileId;
+}
+

+ 46 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/EmailInfoDTO.java

@@ -0,0 +1,46 @@
+package com.smppw.modaq.domain.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class EmailInfoDTO {
+    /**
+     * 邮件id
+     */
+    private Integer id;
+    /**
+     * 文件id
+     */
+    private Integer fileId;
+    /**
+     * 邮箱地址
+     */
+    private String email;
+    /**
+     * 邮箱日期
+     */
+    private Date emailDate;
+    /**
+     * 邮件主题
+     */
+    private String emailTitle;
+    /**
+     * 邮件类型,1-净值,2-估值表,3-定期报告
+     */
+    private Integer emailType;
+    /**
+     * 解析状态
+     */
+    private Integer parseStatus;
+    /**
+     * 文件名称
+     */
+    private String fileName;
+
+    /**
+     * 文件地址
+     */
+    private String filePath;
+}

+ 17 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/EmailZipFileDTO.java

@@ -0,0 +1,17 @@
+package com.smppw.modaq.domain.dto;
+
+import cn.hutool.core.io.FileUtil;
+import lombok.Getter;
+
+@Getter
+public class EmailZipFileDTO {
+    private final String filename;
+    private final String filepath;
+    private final Integer emailType;
+
+    public EmailZipFileDTO(String filepath, Integer emailType) {
+        this.filepath = filepath;
+        this.emailType = emailType;
+        this.filename = FileUtil.getName(filepath);
+    }
+}

+ 38 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/MailboxInfoDTO.java

@@ -0,0 +1,38 @@
+package com.smppw.modaq.domain.dto;
+
+import lombok.Data;
+
+@Data
+public class MailboxInfoDTO {
+
+    /**
+     * 用户id
+     */
+    private Integer userId;
+
+    /**
+     * 邮箱账号
+     */
+    private String account;
+
+    /**
+     * 邮箱密码
+     */
+    private String password;
+
+    /**
+     * 邮箱地址
+     */
+    private String host;
+
+    /**
+     * 端口
+     */
+    private String port;
+
+    /**
+     * 协议
+     */
+    private String protocol;
+
+}

+ 50 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/QuartzBean.java

@@ -0,0 +1,50 @@
+package com.smppw.modaq.domain.dto;
+
+/**
+ * FileName: QuartzBean
+ * Author:   chenjianhua
+ * Date:     2024/9/17 10:26
+ * Description: ${DESCRIPTION}
+ */
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class QuartzBean implements Serializable {
+    /**
+     * 任务id
+     */
+    private String id;
+
+    /**
+     * 任务名称
+     */
+    private String jobName;
+
+    /**
+     * 任务执行类
+     */
+    private String jobClass;
+
+    /**
+     * 组名
+     */
+    private String groupName;
+
+    /**
+     * 任务 参数信息
+     */
+    private String jobParam;
+
+    /**
+     * 任务状态 启动还是暂停
+     */
+    private Integer status;
+
+    /**
+     * 任务运行时间表达式
+     */
+    private String cronExpression;
+}

+ 18 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/AnnuallyReportData.java

@@ -0,0 +1,18 @@
+//package com.smppw.modaq.infrastructure.dto.report;
+//
+//import com.simuwang.base.common.enums.ReportType;
+//import lombok.Getter;
+//import lombok.Setter;
+//
+//@Setter
+//@Getter
+//public class AnnuallyReportData extends QuarterlyReportData {
+//    public AnnuallyReportData(ReportBaseInfoDTO baseInfo, ReportFundInfoDTO fundInfo) {
+//        super(baseInfo, fundInfo);
+//    }
+//
+//    @Override
+//    public ReportType getReportType() {
+//        return ReportType.ANNUALLY;
+//    }
+//}

+ 89 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/BaseReportDTO.java

@@ -0,0 +1,89 @@
+package com.smppw.modaq.domain.dto.report;
+
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import com.smppw.modaq.common.conts.Constants;
+import com.smppw.modaq.domain.entity.report.BaseReportDO;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * @author wangzaijun
+ * @date 2024/10/9 11:08
+ * @description 抽象的报告数据父类,全部字段用string传递
+ */
+@Setter
+@Getter
+public abstract class BaseReportDTO<T extends BaseReportDO> implements Serializable {
+    @Serial
+    private static final long serialVersionUID = Constants.DEFAULT_SERIAL_ID;
+
+    private Integer fileId;
+
+    public BaseReportDTO() {
+    }
+
+    public BaseReportDTO(Integer fileId) {
+        this.fileId = fileId;
+    }
+
+    public abstract T toEntity();
+
+    protected void initEntity(T entity) {
+        entity.setCreatorId(0);
+        entity.setCreateTime(new Date());
+        entity.setUpdaterId(0);
+        entity.setUpdateTime(new Date());
+        entity.setValid(1);
+    }
+
+    @Override
+    public String toString() {
+        return "fileId=" + fileId;
+    }
+
+    /**
+     * 字符串转日期类型
+     *
+     * @param input 待转换的字符串
+     * @return /
+     */
+    protected Date toDate(String input) {
+        if (StrUtil.isBlank(input)) {
+            return null;
+        }
+        try {
+            // 日期格式化,支持三种格式:yyyy年MM月dd日、yyyy-MM-dd和yyyy/MM/dd
+            return DateUtil.parse(input.trim(),
+                    DatePattern.CHINESE_DATE_PATTERN, DatePattern.NORM_DATE_PATTERN, "yyyy/MM/dd", "yyyyMMdd");
+        } catch (Exception ignored) {
+        }
+        return null;
+    }
+
+    /**
+     * 字符串转数字,如果数据没有或者转换失败则用0填充
+     *
+     * @param input 待转换的字符串
+     * @return /
+     */
+    protected BigDecimal toBigDecimal(String input) {
+        if (StrUtil.isBlank(input)) {
+            return BigDecimal.ZERO;
+        }
+        try {
+            // 替换掉非正常的正负小数字符
+            String cleanedInput = input.trim().replaceAll("[^\\s" + "[-+]?\\d*.+" + "]", "");
+            // 创建BigDecimal对象
+            return new BigDecimal(cleanedInput);
+        } catch (NumberFormatException ignored) {
+        }
+        return BigDecimal.ZERO;
+    }
+}

+ 32 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/BaseReportLevelDTO.java

@@ -0,0 +1,32 @@
+//package com.smppw.modaq.domain.dto.report;
+//
+//import com.smppw.modaq.domain.entity.report.BaseReportDO;
+//import lombok.Getter;
+//import lombok.Setter;
+//
+//@Setter
+//@Getter
+//public abstract class BaseReportLevelDTO<T extends BaseReportDO> extends BaseReportDTO<T> {
+//    /**
+//     * 基金分级
+//     */
+//    private String level;
+//
+//    public BaseReportLevelDTO() {
+//        super();
+//    }
+//
+//    public BaseReportLevelDTO(Integer fileId) {
+//        super(fileId);
+//    }
+//
+//    public BaseReportLevelDTO(Integer fileId, String level) {
+//        super(fileId);
+//        this.level = level;
+//    }
+//
+//    @Override
+//    public String toString() {
+//        return super.toString() + ", level='" + this.level + "'";
+//    }
+//}

+ 30 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/LetterReportData.java

@@ -0,0 +1,30 @@
+package com.smppw.modaq.domain.dto.report;
+
+import com.smppw.modaq.common.enums.ReportType;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public class LetterReportData extends ReportData {
+    private ReportInvestorInfoDTO investorInfo;
+    private ReportFundTransactionDTO fundTransaction;
+
+    public LetterReportData(ReportBaseInfoDTO baseInfo, ReportFundInfoDTO fundInfo) {
+        super(baseInfo, fundInfo);
+    }
+
+    @Override
+    public ReportType getReportType() {
+        return ReportType.LETTER;
+    }
+
+    @Override
+    public String toString() {
+        return "{" +
+                super.toString() +
+                ", investorInfo=" + investorInfo +
+                ", fundTransaction=" + fundTransaction +
+                '}';
+    }
+}

+ 25 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/MonthlyReportData.java

@@ -0,0 +1,25 @@
+//package com.smppw.modaq.infrastructure.dto.report;
+//
+//import com.smppw.modaq.common.enums.ReportType;
+//import lombok.Getter;
+//import lombok.Setter;
+//
+//@Setter
+//@Getter
+//public class MonthlyReportData extends ReportData {
+////    private List<ReportNetReportDTO> netReport;
+//
+//    public MonthlyReportData(ReportBaseInfoDTO baseInfo, ReportFundInfoDTO fundInfo) {
+//        super(baseInfo, fundInfo);
+//    }
+//
+//    @Override
+//    public ReportType getReportType() {
+//        return ReportType.MONTHLY;
+//    }
+//
+//    @Override
+//    public String toString() {
+//        return super.toString();
+//    }
+//}

+ 28 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ParseResult.java

@@ -0,0 +1,28 @@
+package com.smppw.modaq.domain.dto.report;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2024/10/10 14:08
+ * @description 报告解析结果
+ */
+@Setter
+@Getter
+public class ParseResult<T extends ReportData> {
+    private Integer status;
+
+    private String msg;
+
+    private T data;
+
+    @Override
+    public String toString() {
+        return "{" +
+                "status=" + status +
+                ", msg='" + msg + '\'' +
+                ", data=" + data +
+                '}';
+    }
+}

+ 41 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/QuarterlyReportData.java

@@ -0,0 +1,41 @@
+//package com.smppw.modaq.infrastructure.dto.report;
+//
+//import com.simuwang.base.common.enums.ReportType;
+//import lombok.Getter;
+//import lombok.Setter;
+//
+//import java.util.List;
+//
+///**
+// * @author wangzaijun
+// * @date 2024/9/26 17:24
+// * @description 季报
+// */
+//@Setter
+//@Getter
+//public class QuarterlyReportData extends ReportData {
+//    private List<ReportAssetAllocationDTO> assetAllocation;
+//    private List<ReportFinancialIndicatorsDTO> financialIndicators;
+//    private List<ReportInvestmentIndustryDTO> investmentIndustry;
+//    private List<ReportShareChangeDTO> shareChange;
+//
+//    public QuarterlyReportData(ReportBaseInfoDTO baseInfo, ReportFundInfoDTO fundInfo) {
+//        super(baseInfo, fundInfo);
+//    }
+//
+//    @Override
+//    public ReportType getReportType() {
+//        return ReportType.QUARTERLY;
+//    }
+//
+//    @Override
+//    public String toString() {
+//        return "{" +
+//                super.toString() +
+//                ", assetAllocation=" + assetAllocation +
+//                ", financialIndicators=" + financialIndicators +
+//                ", investmentIndustry=" + investmentIndustry +
+//                ", shareChange=" + shareChange +
+//                '}';
+//    }
+//}

+ 61 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportAssetAllocationDTO.java

@@ -0,0 +1,61 @@
+//package com.smppw.modaq.infrastructure.dto.report;
+//
+//import com.simuwang.base.pojo.dos.report.ReportAssetAllocationDO;
+//import lombok.Getter;
+//import lombok.Setter;
+//
+///**
+// * @author wangzaijun
+// * @date 2024/9/26 16:43
+// * @description 基金资产组合情况
+// */
+//@Setter
+//@Getter
+//public class ReportAssetAllocationDTO extends BaseReportDTO<ReportAssetAllocationDO> {
+//    /**
+//     * 资产大类
+//     */
+//    private String assetType;
+//    /**
+//     * 资产明细
+//     */
+//    private String assetDetails;
+//    /**
+//     * 市值
+//     */
+//    private String marketValue;
+//    /**
+//     * 备注
+//     */
+//    private String remark;
+//
+//    public ReportAssetAllocationDTO() {
+//        super();
+//    }
+//
+//    public ReportAssetAllocationDTO(Integer fileId) {
+//        super(fileId);
+//    }
+//
+//    @Override
+//    public ReportAssetAllocationDO toEntity() {
+//        ReportAssetAllocationDO entity = new ReportAssetAllocationDO();
+//        entity.setFileId(this.getFileId());
+//        entity.setAssetType(this.assetType);
+//        entity.setColumnName(this.assetDetails);
+//        entity.setMarketValue(this.toBigDecimal(this.marketValue));
+//        entity.setRemark(this.remark);
+//        return entity;
+//    }
+//
+//    @Override
+//    public String toString() {
+//        return "{" +
+//                super.toString() +
+//                ", assetType='" + assetType + '\'' +
+//                ", assetDetails='" + assetDetails + '\'' +
+//                ", marketValue=" + marketValue +
+//                ", remark='" + remark + '\'' +
+//                '}';
+//    }
+//}

+ 56 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportBaseInfoDTO.java

@@ -0,0 +1,56 @@
+package com.smppw.modaq.domain.dto.report;
+
+import com.smppw.modaq.domain.entity.report.ReportBaseInfoDO;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/26 16:44
+ * @description 基协报告基础信息表
+ */
+@Setter
+@Getter
+public class ReportBaseInfoDTO extends BaseReportDTO<ReportBaseInfoDO> {
+    /**
+     * 报告日期
+     */
+    private String reportDate;
+    /**
+     * 报告名称
+     */
+    private String reportName;
+    /**
+     * 报告类型
+     */
+    private String reportType;
+
+    public ReportBaseInfoDTO() {
+        super();
+    }
+
+    public ReportBaseInfoDTO(Integer fileId) {
+        super(fileId);
+    }
+
+    @Override
+    public ReportBaseInfoDO toEntity() {
+        ReportBaseInfoDO entity = new ReportBaseInfoDO();
+        entity.setFileId(this.getFileId());
+        entity.setReportDate(this.toDate(this.reportDate));
+        entity.setReportName(this.reportName);
+        entity.setReportType(this.reportType);
+        this.initEntity(entity);
+        return entity;
+    }
+
+    @Override
+    public String toString() {
+        return "{" +
+                super.toString() +
+                ", reportDate='" + reportDate + '\'' +
+                ", reportName='" + reportName + '\'' +
+                ", reportType='" + reportType + '\'' +
+                '}';
+    }
+}

+ 55 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportData.java

@@ -0,0 +1,55 @@
+package com.smppw.modaq.domain.dto.report;
+
+import com.smppw.modaq.common.conts.Constants;
+import com.smppw.modaq.common.enums.ReportType;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/29 9:32
+ * @description 报告解析结果对象
+ */
+@Getter
+public abstract class ReportData implements Serializable {
+    @Serial
+    private static final long serialVersionUID = Constants.DEFAULT_SERIAL_ID;
+    /**
+     * 报告基本信息
+     */
+    private final ReportBaseInfoDTO baseInfo;
+    /**
+     * 报告包含的基金基本新
+     */
+    private final ReportFundInfoDTO fundInfo;
+    /**
+     * 是否ai解析的结果
+     */
+    @Setter
+    private Boolean aiParse;
+    /**
+     * ai解析时上传的文件的id(方便重复使用)
+     */
+    @Setter
+    private String aiFileId;
+
+    public ReportData(ReportBaseInfoDTO baseInfo, ReportFundInfoDTO fundInfo) {
+        this.baseInfo = baseInfo;
+        this.fundInfo = fundInfo;
+        this.aiParse = false;
+    }
+
+    public abstract ReportType getReportType();
+
+    @Override
+    public String toString() {
+        return "baseInfo=" + baseInfo +
+                ", fundInfo=" + fundInfo +
+                ", aiParse=" + aiParse +
+                ", aiFileId=" + aiFileId +
+                ", reportType=" + this.getReportType();
+    }
+}

+ 92 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFinancialIndicatorsDTO.java

@@ -0,0 +1,92 @@
+//package com.smppw.modaq.infrastructure.dto.report;
+//
+//import cn.hutool.core.util.StrUtil;
+//import com.simuwang.base.pojo.dos.report.ReportFinancialIndicatorsDO;
+//import lombok.Getter;
+//import lombok.Setter;
+//
+//import java.util.regex.Matcher;
+//import java.util.regex.Pattern;
+//
+//@Setter
+//@Getter
+//public class ReportFinancialIndicatorsDTO extends BaseReportLevelDTO<ReportFinancialIndicatorsDO> {
+//    /**
+//     * 年度
+//     */
+//    private String yearly;
+//    /**
+//     * 期末基金净资产
+//     */
+//    private String assetNet;
+//    /**
+//     * 报告期期末单位净值
+//     */
+//    private String nav;
+//    /**
+//     * 本期利润
+//     */
+//    private String profit;
+//    /**
+//     * 本期已实现收益
+//     */
+//    private String realizedIncome;
+//    /**
+//     * 期末可供分配利润
+//     */
+//    private String undistributedProfit;
+//    /**
+//     * 期末可供分配基金份额利润
+//     */
+//    private String undistributedShareProfit;
+//    /**
+//     * 基金份额累计净值增长率
+//     */
+//    private String shareNavRet;
+//
+//    public ReportFinancialIndicatorsDTO() {
+//        super();
+//    }
+//
+//    public ReportFinancialIndicatorsDTO(Integer fileId) {
+//        super(fileId);
+//    }
+//
+//    public ReportFinancialIndicatorsDTO(Integer fileId, String level) {
+//        super(fileId, level);
+//    }
+//
+//    @Override
+//    public ReportFinancialIndicatorsDO toEntity() {
+//        ReportFinancialIndicatorsDO entity = new ReportFinancialIndicatorsDO();
+//        entity.setFileId(this.getFileId());
+//        entity.setLevel(this.getLevel());
+//        entity.setFundAssetSize(this.toBigDecimal(this.assetNet));
+//        entity.setNav(this.toBigDecimal(this.nav));
+//        entity.setProfit(this.toBigDecimal(this.profit));
+//        entity.setRealizedIncome(this.toBigDecimal(this.realizedIncome));
+//        entity.setUndistributedProfit(this.toBigDecimal(this.undistributedProfit));
+//        if (StrUtil.isNotBlank(this.yearly)) {
+//            Matcher matcher = Pattern.compile("\\d+").matcher(this.yearly);
+//            if (matcher.find()) {
+//                entity.setEndDate(Integer.parseInt(matcher.group()));
+//            }
+//        }
+//        return entity;
+//    }
+//
+//    @Override
+//    public String toString() {
+//        return "{" +
+//                super.toString() +
+//                ", yearly=" + yearly +
+//                ", assetNet=" + assetNet +
+//                ", nav=" + nav +
+//                ", profit=" + profit +
+//                ", undistributedProfit=" + undistributedProfit +
+//                ", realizedIncome=" + realizedIncome +
+//                ", undistributedShareProfit=" + undistributedShareProfit +
+//                ", shareNavRet=" + shareNavRet +
+//                '}';
+//    }
+//}

+ 65 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFundInfoDTO.java

@@ -0,0 +1,65 @@
+package com.smppw.modaq.domain.dto.report;
+
+import com.smppw.modaq.domain.entity.report.ReportFundInfoDO;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/26 16:47
+ * @description 报告基金基本信息
+ */
+@Setter
+@Getter
+public class ReportFundInfoDTO extends BaseReportDTO<ReportFundInfoDO> {
+    /**
+     * 基金的名称
+     */
+    private String fundName;
+
+    /**
+     * 基金的唯一识别代码
+     */
+    private String fundCode;
+
+    /**
+     * 基金管理人的名称
+     */
+    private String companyName;
+
+    /**
+     * 基金交易使用的货币种类
+     */
+    private String currency;
+
+    public ReportFundInfoDTO() {
+        super();
+    }
+
+    public ReportFundInfoDTO(Integer fileId) {
+        super(fileId);
+    }
+
+    @Override
+    public ReportFundInfoDO toEntity() {
+        ReportFundInfoDO entity = new ReportFundInfoDO();
+        entity.setFileId(this.getFileId());
+        entity.setFundCode(this.fundCode);
+        entity.setCompanyName(this.companyName);
+        entity.setCurrency(this.currency);
+        entity.setFundName(this.fundName);
+        this.initEntity(entity);
+        return entity;
+    }
+
+    @Override
+    public String toString() {
+        return "{" +
+                super.toString() +
+                ", fundName='" + fundName + '\'' +
+                ", fundCode='" + fundCode + '\'' +
+                ", companyName='" + companyName + '\'' +
+                ", currency='" + currency + '\'' +
+                '}';
+    }
+}

+ 293 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportFundTransactionDTO.java

@@ -0,0 +1,293 @@
+package com.smppw.modaq.domain.dto.report;
+
+import com.smppw.modaq.domain.entity.report.ReportFundTransactionDO;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public class ReportFundTransactionDTO extends BaseReportDTO<ReportFundTransactionDO> {
+    /**
+     * 基金账户编号
+     */
+    private String fundAccount;
+
+    /**
+     * 基金名称
+     */
+    private String fundName;
+
+    /**
+     * 销售此基金产品的经销商或银行
+     */
+    private String distributor;
+
+    /**
+     * 业务类型(例如:申购、赎回)
+     */
+    private String transactionType;
+
+    /**
+     * 业务操作的原因或类型说明
+     */
+    private String businessReason;
+
+    /**
+     * 交易确认的状态(例如:已确认、待处理等)
+     */
+    private String status;
+
+    /**
+     * 交易确认的日期
+     */
+    private String holdingDate;
+
+    /**
+     * 申请的日期
+     */
+    private String applyDate;
+
+    /**
+     * 申请的金额
+     */
+    private String applyAmount;
+
+    /**
+     * 申请的基金份额数量
+     */
+    private String applyShare;
+
+    /**
+     * 确认的金额
+     */
+    private String amount;
+
+    /**
+     * 确认的基金份额数量
+     */
+    private String share;
+
+    /**
+     * 净认购/申购的金额,确认净额
+     */
+    private String netAmount;
+
+    /**
+     * 单位净值
+     */
+    private String nav;
+
+    /**
+     * 确认比例
+     */
+    private String confirmationRatio;
+
+    /**
+     * 交易授权确认编号
+     */
+    private String taConfirmationNumber;
+
+    /**
+     * TA代码
+     */
+    private String taNumber;
+
+    /**
+     * 申请单号
+     */
+    private String applyNo;
+
+    /**
+     * 份额余额
+     */
+    private String shareBalance;
+
+    /**
+     * 份额类别
+     */
+    private String shareCategory;
+
+    /**
+     * 巨额赎回方式
+     */
+    private String largeRedemptionType;
+
+    /**
+     * 提成或保底标志
+     */
+    private String rewardMark;
+
+    /**
+     * 持有天数
+     */
+    private String holdingDays;
+
+    /**
+     * 份额明细注册日期
+     */
+    private String shareRegistryDate;
+
+    // -- 费用 ----------------------------------------
+
+    /**
+     * 总费用
+     */
+    private String fee;
+
+    /**
+     * 利息
+     */
+    private String interest;
+
+    /**
+     * 利息转份额/利息归基金资产
+     */
+    private String interestToFundAssets;
+
+    /**
+     * 交易费
+     */
+    private String tradeFee;
+
+    /**
+     * 违约金
+     */
+    private String defaultFee;
+
+    /**
+     * 业绩报酬
+     */
+    private String performanceFee;
+
+    /**
+     * 费用折扣
+     */
+    private String feeDiscounts;
+
+    /**
+     * 业绩报酬折扣
+     */
+    private String performanceFeeDiscounts;
+
+    // ---- 分红 -----------------------------------------------
+
+    /**
+     * 分红方式
+     */
+    private String dividendType;
+
+    /**
+     * 分红登记日
+     */
+    private String dividendRegistryDate;
+
+    /**
+     * 红利发放日
+     */
+    private String dividendPaymentDate;
+
+    /**
+     * 分红基数份额
+     */
+    private String baseShareDividend;
+
+    /**
+     * 分红模式
+     */
+    private String dividendMode;
+
+    /**
+     * 单位分红
+     */
+    private String unitDividend;
+
+    /**
+     * 每单位分红
+     */
+    private String dividendPerUnit;
+
+    /**
+     * 红利总额
+     */
+    private String totalDividendAmount;
+
+    /**
+     * 实发现金红利
+     */
+    private String actualCashDividend;
+
+    /**
+     * 冻结份额
+     */
+    private String frozenShares;
+
+    /**
+     * 冻结金额
+     */
+    private String frozenAmount;
+
+    /**
+     * 实际业绩提成金额
+     */
+    private String actualPerformanceAmount;
+
+    /**
+     * 实际提成份额
+     */
+    private String actualPerformanceShare;
+
+    @Override
+    public ReportFundTransactionDO toEntity() {
+        ReportFundTransactionDO entity = new ReportFundTransactionDO();
+        entity.setFileId(this.getFileId());
+        entity.setFundAccount(fundAccount);
+        entity.setFundName(fundName);
+        entity.setDistributor(distributor);
+        entity.setTransactionType(transactionType);
+        entity.setBusinessReason(businessReason);
+        entity.setStatus(status);
+        entity.setHoldingDate(this.toDate(holdingDate));
+        entity.setApplyDate(this.toDate(applyDate));
+        entity.setApplyAmount(this.toBigDecimal(applyAmount));
+        entity.setApplyShare(this.toBigDecimal(applyShare));
+        entity.setAmount(this.toBigDecimal(amount));
+        entity.setShare(this.toBigDecimal(share));
+        entity.setNetAmount(this.toBigDecimal(netAmount));
+        entity.setNav(this.toBigDecimal(nav));
+        entity.setConfirmationRatio(this.toBigDecimal(confirmationRatio));
+        entity.setTaConfirmationNumber(taConfirmationNumber);
+        entity.setTaNumber(taNumber);
+        entity.setApplyNo(applyNo);
+        entity.setShareBalance(this.toBigDecimal(shareBalance));
+        entity.setShareCategory(shareCategory);
+        entity.setLargeRedemptionType(largeRedemptionType);
+        entity.setRewardMark(rewardMark);
+        entity.setHoldingDays(holdingDays);
+        entity.setShareRegistryDate(this.toDate(shareRegistryDate));
+        // --- 以下是费用
+        entity.setFee(this.toBigDecimal(fee));
+        entity.setInterest(this.toBigDecimal(interest));
+        entity.setInterestToFundAssets(this.toBigDecimal(interestToFundAssets));
+        entity.setTradeFee(this.toBigDecimal(tradeFee));
+        entity.setDefaultFee(this.toBigDecimal(defaultFee));
+        entity.setPerformanceFee(this.toBigDecimal(performanceFee));
+        entity.setFeeDiscounts(this.toBigDecimal(feeDiscounts));
+        entity.setPerformanceFeeDiscounts(this.toBigDecimal(performanceFeeDiscounts));
+        // --- 以下是分红
+        entity.setDividendType(dividendType);
+        entity.setDividendRegistryDate(this.toDate(dividendRegistryDate));
+        entity.setDividendPaymentDate(this.toDate(dividendPaymentDate));
+        entity.setBaseShareDividend(this.toBigDecimal(baseShareDividend));
+        entity.setDividendMode(dividendMode);
+        entity.setUnitDividend(this.toBigDecimal(unitDividend));
+        entity.setDividendPerUnit(this.toBigDecimal(dividendPerUnit));
+        entity.setTotalDividendAmount(this.toBigDecimal(totalDividendAmount));
+        entity.setActualCashDividend(this.toBigDecimal(actualCashDividend));
+        entity.setFrozenShares(this.toBigDecimal(frozenShares));
+        entity.setFrozenAmount(this.toBigDecimal(frozenAmount));
+        entity.setActualPerformanceAmount(this.toBigDecimal(actualPerformanceAmount));
+        entity.setActualPerformanceShare(this.toBigDecimal(actualPerformanceShare));
+        this.initEntity(entity);
+        return entity;
+    }
+}

+ 61 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportInvestmentIndustryDTO.java

@@ -0,0 +1,61 @@
+//package com.smppw.modaq.infrastructure.dto.report;
+//
+//import com.simuwang.base.pojo.dos.report.ReportInvestmentIndustryDO;
+//import lombok.Getter;
+//import lombok.Setter;
+//
+///**
+// * @author wangzaijun
+// * @date 2024/9/26 16:49
+// * @description 按行业分类的股票投资组合
+// */
+//@Setter
+//@Getter
+//public class ReportInvestmentIndustryDTO extends BaseReportDTO<ReportInvestmentIndustryDO> {
+//    /**
+//     * 行业分类名称
+//     */
+//    private String industryName;
+//    /**
+//     * 投资地区: 1-境内, 2-港股通
+//     */
+//    private Integer investType;
+//    /**
+//     * 公允价值,市值
+//     */
+//    private String marketValue;
+//    /**
+//     * 占基金资产净值的比例,占净值比,权重
+//     */
+//    private String ratio;
+//
+//    public ReportInvestmentIndustryDTO() {
+//        super();
+//    }
+//
+//    public ReportInvestmentIndustryDTO(Integer fileId) {
+//        super(fileId);
+//    }
+//
+//    @Override
+//    public ReportInvestmentIndustryDO toEntity() {
+//        ReportInvestmentIndustryDO entity = new ReportInvestmentIndustryDO();
+//        entity.setFileId(this.getFileId());
+//        entity.setIndustryName(this.industryName);
+//        entity.setInvestType(this.investType);
+//        entity.setMarketValue(this.toBigDecimal(this.marketValue));
+//        entity.setRatio(this.toBigDecimal(this.ratio));
+//        return entity;
+//    }
+//
+//    @Override
+//    public String toString() {
+//        return "{" +
+//                super.toString() +
+//                ", industryName='" + industryName + '\'' +
+//                ", investType=" + investType +
+//                ", marketValue=" + marketValue +
+//                ", ratio=" + ratio +
+//                '}';
+//    }
+//}

+ 53 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportInvestorInfoDTO.java

@@ -0,0 +1,53 @@
+package com.smppw.modaq.domain.dto.report;
+
+import com.smppw.modaq.domain.entity.report.ReportInvestorInfoDO;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public class ReportInvestorInfoDTO extends BaseReportDTO<ReportInvestorInfoDO> {
+    /**
+     * 投资人的姓名
+     */
+    private String investorName;
+
+    /**
+     * 投资人的类别(例如:个人、机构)
+     */
+    private String investorType;
+
+    /**
+     * 证件类型(例如:身份证、护照)
+     */
+    private String certificateType;
+
+    /**
+     * 投资人证件号码
+     */
+    private String certificateNumber;
+
+    /**
+     * 基金账户编号
+     */
+    private String fundAccount;
+
+    /**
+     * 投资者交易账号
+     */
+    private String tradingAccount;
+
+    @Override
+    public ReportInvestorInfoDO toEntity() {
+        ReportInvestorInfoDO entity = new ReportInvestorInfoDO();
+        entity.setFileId(this.getFileId());
+        entity.setInvestorName(this.investorName);
+        entity.setInvestorType(this.investorType);
+        entity.setCertificateType(this.certificateType);
+        entity.setCertificateNumber(this.certificateNumber);
+        entity.setFundAccount(this.fundAccount);
+        entity.setTradingAccount(this.tradingAccount);
+        this.initEntity(entity);
+        return entity;
+    }
+}

+ 72 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportNetReportDTO.java

@@ -0,0 +1,72 @@
+//package com.smppw.modaq.infrastructure.dto.report;
+//
+//import com.simuwang.base.pojo.dos.report.ReportNetReportDO;
+//import lombok.Getter;
+//import lombok.Setter;
+//
+///**
+// * @author wangzaijun
+// * @date 2024/9/26 16:53
+// * @description 基协报告净值月报
+// */
+//@Setter
+//@Getter
+//public class ReportNetReportDTO extends BaseReportLevelDTO<ReportNetReportDO> {
+//    /**
+//     * 估值日期
+//     */
+//    private String valuationDate;
+//    /**
+//     * 累计净值
+//     */
+//    private String cumulativeNavWithdrawal;
+//    /**
+//     * 基金份额总额
+//     */
+//    private String assetShare;
+//    /**
+//     * 基金资产净值
+//     */
+//    private String assetNet;
+//    /**
+//     * 单位净值
+//     */
+//    private String nav;
+//
+//    public ReportNetReportDTO() {
+//        super();
+//    }
+//
+//    public ReportNetReportDTO(Integer fileId) {
+//        super(fileId);
+//    }
+//
+//    public ReportNetReportDTO(Integer fileId, String level) {
+//        super(fileId, level);
+//    }
+//
+//    @Override
+//    public ReportNetReportDO toEntity() {
+//        ReportNetReportDO entity = new ReportNetReportDO();
+//        entity.setFileId(this.getFileId());
+//        entity.setLevel(this.getLevel());
+//        entity.setValuationDate(this.toDate(this.valuationDate));
+//        entity.setCumulativeNav(this.toBigDecimal(this.cumulativeNavWithdrawal));
+//        entity.setEndTotalShares(this.toBigDecimal(this.assetShare));
+//        entity.setFundAssetSize(this.toBigDecimal(this.assetNet));
+//        entity.setNav(this.toBigDecimal(this.nav));
+//        return entity;
+//    }
+//
+//    @Override
+//    public String toString() {
+//        return "{" +
+//                super.toString() +
+//                ", valuationDate='" + valuationDate + '\'' +
+//                ", cumulativeNavWithdrawal=" + cumulativeNavWithdrawal +
+//                ", assetShare=" + assetShare +
+//                ", fundAssetSize=" + assetNet +
+//                ", nav=" + nav +
+//                '}';
+//    }
+//}

+ 34 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportParserParams.java

@@ -0,0 +1,34 @@
+package com.smppw.modaq.domain.dto.report;
+
+import com.smppw.modaq.common.enums.ReportType;
+import lombok.*;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+public class ReportParserParams {
+    /**
+     * 文件id
+     * 报告解析表的关联字段
+     */
+    private Integer fileId;
+    /**
+     * 文件名称
+     * 优先从这个名称里先获取基金备案编码,没有就不获取
+     */
+    private String filename;
+    /**
+     * 文件路径
+     */
+    private String filepath;
+    /**
+     * 备案编码
+     */
+    private String registerNumber;
+
+    private String aiFileId;
+
+    private ReportType reportType;
+}

+ 72 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/dto/report/ReportShareChangeDTO.java

@@ -0,0 +1,72 @@
+//package com.smppw.modaq.infrastructure.dto.report;
+//
+//import com.simuwang.base.pojo.dos.report.ReportShareChangeDO;
+//import lombok.Getter;
+//import lombok.Setter;
+//
+///**
+// * @author wangzaijun
+// * @date 2024/9/26 16:40
+// * @description 基金份额变动情况
+// */
+//@Setter
+//@Getter
+//public class ReportShareChangeDTO extends BaseReportLevelDTO<ReportShareChangeDO> {
+//    /**
+//     * 报告期期初基金份额总额
+//     */
+//    private String initTotalShares;
+//    /**
+//     * 减: 报告期期间基金总赎回份额
+//     */
+//    private String redemption;
+//    /**
+//     * 期末基金总份额/期末基金实缴总额
+//     */
+//    private String sharePerAsset;
+//    /**
+//     * 报告期期间基金拆分变动份额
+//     */
+//    private String splitChangeShare;
+//    /**
+//     * 报告期期间基金总申购份额
+//     */
+//    private String subscription;
+//
+//    public ReportShareChangeDTO() {
+//        super();
+//    }
+//
+//    public ReportShareChangeDTO(Integer fileId) {
+//        super(fileId);
+//    }
+//
+//    public ReportShareChangeDTO(Integer fileId, String level) {
+//        super(fileId, level);
+//    }
+//
+//    @Override
+//    public ReportShareChangeDO toEntity() {
+//        ReportShareChangeDO entity = new ReportShareChangeDO();
+//        entity.setFileId(this.getFileId());
+//        entity.setLevel(this.getLevel());
+//        entity.setRedemption(this.toBigDecimal(this.redemption));
+//        entity.setInitTotalShares(this.toBigDecimal(this.initTotalShares));
+//        entity.setSharePerAsset(this.toBigDecimal(this.sharePerAsset));
+//        entity.setSplit(this.toBigDecimal(this.splitChangeShare));
+//        entity.setSubscription(this.toBigDecimal(this.subscription));
+//        return entity;
+//    }
+//
+//    @Override
+//    public String toString() {
+//        return "{" +
+//                super.toString() +
+//                ", initTotalShares=" + initTotalShares +
+//                ", redemption=" + redemption +
+//                ", sharePerAsset=" + sharePerAsset +
+//                ", splitChangeShare=" + splitChangeShare +
+//                ", subscription=" + subscription +
+//                '}';
+//    }
+//}

+ 57 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/entity/EmailFieldMappingDO.java

@@ -0,0 +1,57 @@
+package com.smppw.modaq.domain.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@TableName("mo_email_field_mapping")
+public class EmailFieldMappingDO {
+    /**
+     * 主键Id
+     */
+    @TableId(value = "id")
+    private Integer id;
+    /**
+     * 字段编码
+     */
+    @TableField(value = "code")
+    private String code;
+    /**
+     * 字段(多个以英文逗号隔开)
+     */
+    @TableField(value = "name")
+    private String name;
+    /**
+     * 1-净值或估值表,3-定期报告,0-表示共用的,默认0
+     */
+    private Integer type;
+    /**
+     * 记录的有效性;1-有效;0-无效;
+     */
+    @TableField(value = "isvalid")
+    private Integer isvalid;
+    /**
+     * 创建者Id;第一次创建时与Creator值相同,修改时与修改人值相同
+     */
+    @TableField(value = "creatorid")
+    private Integer creatorId;
+    /**
+     * 修改者Id;第一次创建时与Creator值相同,修改时与修改人值相同
+     */
+    @TableField(value = "updaterid")
+    private Integer updaterId;
+    /**
+     * 创建时间,默认第一次创建的getdate()时间
+     */
+    @TableField(value = "createtime")
+    private Date createTime;
+    /**
+     * 修改时间;第一次创建时与CreatTime值相同,修改时与修改时间相同
+     */
+    @TableField(value = "updatetime")
+    private Date updateTime;
+}

+ 59 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/entity/EmailFileInfoDO.java

@@ -0,0 +1,59 @@
+package com.smppw.modaq.domain.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@TableName("mo_email_file_info")
+public class EmailFileInfoDO {
+    /**
+     * 主键Id
+     */
+    private Integer id;
+    /**
+     * 邮件id(email_parse_info.id)
+     */
+    private Integer emailId;
+    /**
+     * 基金id
+     */
+    private Integer fundId;
+    /**
+     * 附件名称
+     */
+    private String fileName;
+    /**
+     * 附件路径
+     */
+    private String filePath;
+    /**
+     * 是否ai解析
+     */
+    private Boolean aiParse;
+    /**
+     * ai解析时上传的文件的id(方便重复使用)
+     */
+    private String aiFileId;
+    /**
+     * 记录的有效性;1-有效;0-无效;
+     */
+    private Integer isvalid;
+    /**
+     * 创建者Id;第一次创建时与Creator值相同,修改时与修改人值相同
+     */
+    private Integer creatorId;
+    /**
+     * 修改者Id;第一次创建时与Creator值相同,修改时与修改人值相同
+     */
+    private Integer updaterId;
+    /**
+     * 创建时间,默认第一次创建的getdate()时间
+     */
+    private Date createTime;
+    /**
+     * 修改时间;第一次创建时与CreatTime值相同,修改时与修改时间相同
+     */
+    private Date updateTime;
+}

+ 85 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/entity/EmailParseInfoDO.java

@@ -0,0 +1,85 @@
+package com.smppw.modaq.domain.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@TableName("mo_email_parse_info")
+public class EmailParseInfoDO {
+    /**
+     * 主键Id
+     */
+    @TableId(value = "id")
+    private Integer id;
+
+    private String emailKey;
+    /**
+     * 邮件发送方
+     */
+    @TableField(value = "sender_email")
+    private String senderEmail;
+    /**
+     * 邮箱地址
+     */
+    @TableField(value = "email")
+    private String email;
+    /**
+     * 邮箱日期
+     */
+    @TableField(value = "email_date")
+    private Date emailDate;
+    /**
+     * 解析日期
+     */
+    @TableField(value = "parse_date")
+    private Date parseDate;
+    /**
+     * 邮件主题
+     */
+    @TableField(value = "email_title")
+    private String emailTitle;
+    /**
+     * 邮件类型,1-净值,2-估值表,3-定期报告
+     */
+    @TableField(value = "email_type")
+    private Integer emailType;
+    /**
+     * 解析状态
+     */
+    @TableField(value = "parse_status")
+    private Integer parseStatus;
+    /**
+     * 失败原因
+     */
+    @TableField(value = "fail_reason")
+    private String failReason;
+    /**
+     * 记录的有效性;1-有效;0-无效;
+     */
+    @TableField(value = "isvalid")
+    private Integer isvalid;
+    /**
+     * 创建者Id;第一次创建时与Creator值相同,修改时与修改人值相同
+     */
+    @TableField(value = "creatorid")
+    private Integer creatorId;
+    /**
+     * 修改者Id;第一次创建时与Creator值相同,修改时与修改人值相同
+     */
+    @TableField(value = "updaterid")
+    private Integer updaterId;
+    /**
+     * 创建时间,默认第一次创建的getdate()时间
+     */
+    @TableField(value = "createtime")
+    private Date createTime;
+    /**
+     * 修改时间;第一次创建时与CreatTime值相同,修改时与修改时间相同
+     */
+    @TableField(value = "updatetime")
+    private Date updateTime;
+}

+ 93 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/entity/MailboxInfoDO.java

@@ -0,0 +1,93 @@
+package com.smppw.modaq.domain.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@TableName("mo_mailbox_info")
+public class MailboxInfoDO {
+    /**
+     * 主键Id
+     */
+    @TableId(value = "id")
+    private Integer id;
+//    /**
+//     * 用户id
+//     */
+//    @TableField(value = "user_id")
+//    private Integer userId;
+    /**
+     * 邮箱类型:1-QQ邮箱,2-腾讯企业邮箱,3-网易邮箱,4-新浪邮箱,99-其他
+     */
+    @TableField(value = "type")
+    private Integer type;
+    /**
+     * 邮箱账号
+     */
+    @TableField(value = "email")
+    private String email;
+    /**
+     * 邮箱密码
+     */
+    @TableField(value = "password")
+    private String password;
+    /**
+     * 协议
+     */
+    @TableField(value = "protocol")
+    private String protocol;
+    /**
+     * 收件服务器
+     */
+    @TableField(value = "server")
+    private String server;
+    /**
+     * 端口
+     */
+    @TableField(value = "port")
+    private String port;
+    /**
+     * cron表达式
+     */
+    @TableField(value = "cron")
+    private String cron;
+    /**
+     * 是否开启,0-不开启,1-开启
+     */
+    @TableField(value = "open_status")
+    private Integer openStatus;
+    /**
+     * 备注信息
+     */
+    @TableField(value = "description")
+    private String description;
+    /**
+     * 记录的有效性;1-有效;0-无效;
+     */
+    @TableField(value = "isvalid")
+    private Integer isvalid;
+    /**
+     * 创建者Id;第一次创建时与Creator值相同,修改时与修改人值相同
+     */
+    @TableField(value = "creatorid")
+    private Integer creatorId;
+    /**
+     * 修改者Id;第一次创建时与Creator值相同,修改时与修改人值相同
+     */
+    @TableField(value = "updaterid")
+    private Integer updaterId;
+    /**
+     * 创建时间,默认第一次创建的getdate()时间
+     */
+    @TableField(value = "createtime")
+    private Date createTime;
+    /**
+     * 修改时间;第一次创建时与CreatTime值相同,修改时与修改时间相同
+     */
+    @TableField(value = "updatetime")
+    private Date updateTime;
+}

+ 23 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/entity/report/BaseReportDO.java

@@ -0,0 +1,23 @@
+package com.smppw.modaq.domain.entity.report;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.smppw.modaq.common.support.dos.DataEntity;
+import com.smppw.modaq.common.support.vo.BaseVO;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public abstract class BaseReportDO extends DataEntity<BaseVO> {
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    private Integer fileId;
+
+    @Override
+    public BaseVO toVo() {
+        // 没有转换为VO对象的逻辑
+        return null;
+    }
+}

+ 30 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/entity/report/ReportBaseInfoDO.java

@@ -0,0 +1,30 @@
+package com.smppw.modaq.domain.entity.report;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/26 16:44
+ * @description 基协报告基础信息表
+ */
+@Setter
+@Getter
+@TableName("mo_report_base_info")
+public class ReportBaseInfoDO extends BaseReportDO {
+    /**
+     * 报告日期
+     */
+    private Date reportDate;
+    /**
+     * 报告名称
+     */
+    private String reportName;
+    /**
+     * 报告类型
+     */
+    private String reportType;
+}

+ 35 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/entity/report/ReportFundInfoDO.java

@@ -0,0 +1,35 @@
+package com.smppw.modaq.domain.entity.report;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2024/9/26 16:47
+ * @description 报告基金基本信息
+ */
+@Setter
+@Getter
+@TableName("mo_report_fund_info")
+public class ReportFundInfoDO extends BaseReportDO {
+    /**
+     * 基金的名称
+     */
+    private String fundName;
+
+    /**
+     * 基金的唯一识别代码
+     */
+    private String fundCode;
+
+    /**
+     * 基金管理人的名称
+     */
+    private String companyName;
+
+    /**
+     * 基金交易使用的货币种类
+     */
+    private String currency;
+}

+ 242 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/entity/report/ReportFundTransactionDO.java

@@ -0,0 +1,242 @@
+package com.smppw.modaq.domain.entity.report;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Setter
+@Getter
+@TableName("mo_report_fund_transaction")
+public class ReportFundTransactionDO extends BaseReportDO {
+    /**
+     * 基金账户编号
+     */
+    private String fundAccount;
+
+    /**
+     * 基金名称
+     */
+    private String fundName;
+
+    /**
+     * 销售此基金产品的经销商或银行
+     */
+    private String distributor;
+
+    /**
+     * 业务类型(例如:申购、赎回)
+     */
+    private String transactionType;
+
+    /**
+     * 业务操作的原因或类型说明
+     */
+    private String businessReason;
+
+    /**
+     * 交易确认的状态(例如:已确认、待处理等)
+     */
+    private String status;
+
+    /**
+     * 交易确认的日期
+     */
+    private Date holdingDate;
+
+    /**
+     * 申请的日期
+     */
+    private Date applyDate;
+
+    /**
+     * 申请的金额
+     */
+    private BigDecimal applyAmount;
+
+    /**
+     * 申请的基金份额数量
+     */
+    private BigDecimal applyShare;
+
+    /**
+     * 确认的金额
+     */
+    private BigDecimal amount;
+
+    /**
+     * 确认的基金份额数量
+     */
+    private BigDecimal share;
+
+    /**
+     * 净认购/申购的金额,确认净额
+     */
+    private BigDecimal netAmount;
+
+    /**
+     * 单位净值
+     */
+    private BigDecimal nav;
+
+    /**
+     * 确认比例
+     */
+    private BigDecimal confirmationRatio;
+
+    /**
+     * 交易授权确认编号
+     */
+    private String taConfirmationNumber;
+
+    /**
+     * TA代码
+     */
+    private String taNumber;
+
+    /**
+     * 申请单号
+     */
+    private String applyNo;
+
+    /**
+     * 份额余额
+     */
+    private BigDecimal shareBalance;
+
+    /**
+     * 份额类别
+     */
+    private String shareCategory;
+
+    /**
+     * 巨额赎回方式
+     */
+    private String largeRedemptionType;
+
+    /**
+     * 提成或保底标志
+     */
+    private String rewardMark;
+
+    /**
+     * 持有天数
+     */
+    private String holdingDays;
+
+    /**
+     * 份额明细注册日期
+     */
+    private Date shareRegistryDate;
+
+    // -- 费用 ----------------------------------------
+
+    /**
+     * 总费用
+     */
+    private BigDecimal fee;
+
+    /**
+     * 利息
+     */
+    private BigDecimal interest;
+
+    /**
+     * 利息转份额/利息归基金资产
+     */
+    private BigDecimal interestToFundAssets;
+
+    /**
+     * 交易费
+     */
+    private BigDecimal tradeFee;
+
+    /**
+     * 违约金
+     */
+    private BigDecimal defaultFee;
+
+    /**
+     * 业绩报酬
+     */
+    private BigDecimal performanceFee;
+
+    /**
+     * 费用折扣
+     */
+    private BigDecimal feeDiscounts;
+
+    /**
+     * 业绩报酬折扣
+     */
+    private BigDecimal performanceFeeDiscounts;
+
+    // ---- 分红 -----------------------------------------------
+
+    /**
+     * 分红方式
+     */
+    private String dividendType;
+
+    /**
+     * 分红登记日
+     */
+    private Date dividendRegistryDate;
+
+    /**
+     * 红利发放日
+     */
+    private Date dividendPaymentDate;
+
+    /**
+     * 分红基数份额
+     */
+    private BigDecimal baseShareDividend;
+
+    /**
+     * 分红模式
+     */
+    private String dividendMode;
+
+    /**
+     * 单位分红
+     */
+    private BigDecimal unitDividend;
+
+    /**
+     * 每单位分红
+     */
+    private BigDecimal dividendPerUnit;
+
+    /**
+     * 红利总额
+     */
+    private BigDecimal totalDividendAmount;
+
+    /**
+     * 实发现金红利
+     */
+    private BigDecimal actualCashDividend;
+
+    /**
+     * 冻结份额
+     */
+    private BigDecimal frozenShares;
+
+    /**
+     * 冻结金额
+     */
+    private BigDecimal frozenAmount;
+
+    /**
+     * 实际业绩提成金额
+     */
+    private BigDecimal actualPerformanceAmount;
+
+    /**
+     * 实际提成份额
+     */
+    private BigDecimal actualPerformanceShare;
+}

+ 40 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/entity/report/ReportInvestorInfoDO.java

@@ -0,0 +1,40 @@
+package com.smppw.modaq.domain.entity.report;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@TableName("mo_report_investor_info")
+public class ReportInvestorInfoDO extends BaseReportDO {
+    /**
+     * 投资人的姓名
+     */
+    private String investorName;
+
+    /**
+     * 投资人的类别(例如:个人、机构)
+     */
+    private String investorType;
+
+    /**
+     * 证件类型(例如:身份证、护照)
+     */
+    private String certificateType;
+
+    /**
+     * 投资人证件号码
+     */
+    private String certificateNumber;
+
+    /**
+     * 基金账户编号
+     */
+    private String fundAccount;
+
+    /**
+     * 投资者交易账号
+     */
+    private String tradingAccount;
+}

+ 19 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/mapper/EmailFieldMappingMapper.java

@@ -0,0 +1,19 @@
+package com.smppw.modaq.domain.mapper;
+
+import com.smppw.modaq.domain.entity.EmailFieldMappingDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface EmailFieldMappingMapper {
+
+    /**
+     * 获取净值文件字段识别映射配置
+     *
+     * @param types 0-公共的字段,1-净值和估值表解析的字段,3-定期报告解析的字段,4-交易流水确认函
+     * @return 净值文件字段识别映射配置
+     */
+    List<EmailFieldMappingDO> getEmailFieldMapping(List<Integer> types);
+
+}

+ 38 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/mapper/EmailFileInfoMapper.java

@@ -0,0 +1,38 @@
+package com.smppw.modaq.domain.mapper;
+
+import com.smppw.modaq.domain.entity.EmailFileInfoDO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+@Mapper
+public interface EmailFileInfoMapper {
+
+    Integer insert(@Param("itemDo") EmailFileInfoDO emailFileInfoDO);
+
+    EmailFileInfoDO getEmailFileById(@Param("id") Integer fileId);
+
+    List<EmailFileInfoDO> getEmailFileByEmailId(@Param("emailId") Integer emailId);
+
+    List<EmailFileInfoDO> queryByEmailId(@Param("emailId") Integer emailId);
+
+//    List<FundFileInfoVO> searchFundFileInfo(FundFilePageQuery fundFilePageQuery);
+//
+//    List<EmailParseDetailDO> searchEmailDetailById(EmailFileQuery emailFileQuery);
+//
+//    long countFundFileInfo(FundFilePageQuery fundFilePageQuery);
+//
+//    long countEmailDetailById(EmailFileQuery emailFileQuery);
+
+    void updateTimeById(@Param("id") Integer fileId, @Param("parseDate") Date parseDate);
+
+//    List<Integer> selectValuationFileId(@Param("fileIdList") List<Integer> fileIdList);
+//
+//    List<String> getAllPriceDateByFileId(@Param("fileId") Integer fileId);
+
+    int updateAiParseByFileId(@Param("fileId") Integer fileId,
+                              @Param("aiParse") Boolean aiParse,
+                              @Param("aiFileId") String aiFileId);
+}

+ 37 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/mapper/EmailParseInfoMapper.java

@@ -0,0 +1,37 @@
+package com.smppw.modaq.domain.mapper;
+
+import com.smppw.modaq.domain.entity.EmailParseInfoDO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+
+@Mapper
+public interface EmailParseInfoMapper {
+
+    Integer insert(@Param("itemDo") EmailParseInfoDO emailParseInfoDO);
+
+    void updateParseStatus(@Param("id") Integer id, @Param("parseStatus") int parseStatus, @Param("failReason") String failReason);
+
+//    List<EmailParseInfoDO> searchEmailList(EmailParseQuery emailParseQuery);
+
+    EmailParseInfoDO searchEmailById(@Param("id") Integer id);
+
+    Integer searchEmailCount(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("parseStatus") Integer parseStatus);
+
+    EmailParseInfoDO queryById(@Param("id") Integer id);
+
+//    long countEmailList(EmailParseQuery emailParseQuery);
+
+    void updateParseTime(@Param("id") Integer id, @Param("parseDate") Date parseDate);
+
+//    List<EmailInfoDTO> queryValuationEmailByFileId(@Param("fileIdList") List<Integer> fileIdList);
+//
+//    List<Map<String, Object>> searchEmailDataBoard(DataboardQuery databoardQuery);
+//
+//    List<Map<String, Object>> searchEmailTypeCount(DataboardQuery databoardQuery);
+//
+//    Long countpdfNoData(@Param("item") DataboardQuery databoardQuery, @Param("errorInfo")String errorInfo);
+
+    Long countEmailTotal(@Param("emailType") Integer emailType);
+}

+ 31 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/mapper/MailboxInfoMapper.java

@@ -0,0 +1,31 @@
+package com.smppw.modaq.domain.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.smppw.modaq.domain.entity.MailboxInfoDO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface MailboxInfoMapper extends BaseMapper<MailboxInfoDO> {
+
+    /**
+     * 查询配置邮箱信息
+     *
+     * @return 配置邮箱信息
+     */
+    List<MailboxInfoDO> listMailboxInfo();
+
+//    List<MailboxInfoDO> searchEmailConfigList(EmailPageQuery emailPageQuery);
+
+    void deleteEmailConfigByIds(@Param("ids") List<Integer> split, @Param("updaterId") Integer userId);
+
+    MailboxInfoDO selectEmailConfigByEmail(@Param("email") String email);
+
+//    long countEmailConfig(EmailPageQuery emailPageQuery);
+
+    MailboxInfoDO searchEmailConfigById(@Param("id") Integer id);
+
+    List<MailboxInfoDO> getAll();
+}

+ 9 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/mapper/ReportBaseInfoMapper.java

@@ -0,0 +1,9 @@
+package com.smppw.modaq.domain.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.smppw.modaq.domain.entity.report.ReportBaseInfoDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ReportBaseInfoMapper extends BaseMapper<ReportBaseInfoDO> {
+}

+ 9 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/mapper/ReportFundInfoMapper.java

@@ -0,0 +1,9 @@
+package com.smppw.modaq.domain.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.smppw.modaq.domain.entity.report.ReportFundInfoDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ReportFundInfoMapper extends BaseMapper<ReportFundInfoDO> {
+}

+ 9 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/mapper/ReportFundTransactionMapper.java

@@ -0,0 +1,9 @@
+package com.smppw.modaq.domain.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.smppw.modaq.domain.entity.report.ReportFundTransactionDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ReportFundTransactionMapper extends BaseMapper<ReportFundTransactionDO> {
+}

+ 9 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/mapper/ReportInvestorInfoMapper.java

@@ -0,0 +1,9 @@
+package com.smppw.modaq.domain.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.smppw.modaq.domain.entity.report.ReportInvestorInfoDO;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface ReportInvestorInfoMapper extends BaseMapper<ReportInvestorInfoDO> {
+}

+ 517 - 0
mo-daq/src/main/java/com/smppw/modaq/domain/service/EmailParseService.java

@@ -0,0 +1,517 @@
+package com.smppw.modaq.domain.service;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.smppw.modaq.application.components.ReportParseUtils;
+import com.smppw.modaq.application.components.report.parser.ReportParser;
+import com.smppw.modaq.application.components.report.parser.ReportParserFactory;
+import com.smppw.modaq.application.components.report.writer.ReportWriter;
+import com.smppw.modaq.application.components.report.writer.ReportWriterFactory;
+import com.smppw.modaq.application.util.EmailUtil;
+import com.smppw.modaq.common.conts.DateConst;
+import com.smppw.modaq.common.conts.EmailParseStatusConst;
+import com.smppw.modaq.common.conts.EmailTypeConst;
+import com.smppw.modaq.common.enums.ReportParseStatus;
+import com.smppw.modaq.common.enums.ReportParserFileType;
+import com.smppw.modaq.common.enums.ReportType;
+import com.smppw.modaq.common.exception.ReportParseException;
+import com.smppw.modaq.domain.dto.EmailContentInfoDTO;
+import com.smppw.modaq.domain.dto.EmailZipFileDTO;
+import com.smppw.modaq.domain.dto.MailboxInfoDTO;
+import com.smppw.modaq.domain.dto.report.ParseResult;
+import com.smppw.modaq.domain.dto.report.ReportData;
+import com.smppw.modaq.domain.dto.report.ReportParserParams;
+import com.smppw.modaq.domain.entity.EmailFileInfoDO;
+import com.smppw.modaq.domain.entity.EmailParseInfoDO;
+import com.smppw.modaq.domain.mapper.EmailFileInfoMapper;
+import com.smppw.modaq.domain.mapper.EmailParseInfoMapper;
+import com.smppw.modaq.infrastructure.util.ExcelUtil;
+import jakarta.mail.*;
+import jakarta.mail.internet.MimeMessage;
+import jakarta.mail.internet.MimeMultipart;
+import jakarta.mail.search.ComparisonTerm;
+import jakarta.mail.search.ReceivedDateTerm;
+import jakarta.mail.search.SearchTerm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StopWatch;
+
+import java.io.File;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * @author mozuwen
+ * @date 2024-09-04
+ * @description 邮件解析服务
+ */
+@Service
+public class EmailParseService {
+
+    //    public static final int stepSize = 10000;
+    private static final Logger log = LoggerFactory.getLogger(EmailParseService.class);
+
+    //    private final EmailFieldMappingMapper emailFieldMapper;
+    private final EmailParseInfoMapper emailParseInfoMapper;
+    private final EmailFileInfoMapper emailFileInfoMapper;
+    /* 报告解析和入库的方法 */
+    private final ReportParserFactory reportParserFactory;
+    private final ReportWriterFactory reportWriterFactory;
+
+
+    @Value("${email.file.path}")
+    private String path;
+
+
+    public EmailParseService(EmailParseInfoMapper emailParseInfoMapper,
+                             EmailFileInfoMapper emailFileInfoMapper,
+                             ReportParserFactory reportParserFactory,
+                             ReportWriterFactory reportWriterFactory) {
+//        this.emailFieldMapper = emailFieldMapper;
+        this.emailParseInfoMapper = emailParseInfoMapper;
+        this.emailFileInfoMapper = emailFileInfoMapper;
+        this.reportParserFactory = reportParserFactory;
+        this.reportWriterFactory = reportWriterFactory;
+    }
+
+    /**
+     * 解析指定邮箱指定时间范围内的邮件
+     *
+     * @param mailboxInfoDTO 邮箱配置信息
+     * @param startDate      邮件起始日期(yyyy-MM-dd HH:mm:ss)
+     * @param endDate        邮件截止日期(yyyy-MM-dd HH:mm:ss, 为null,将解析邮件日期小于等于startDate的当天邮件)
+     */
+    public void parseEmail(MailboxInfoDTO mailboxInfoDTO, Date startDate, Date endDate) {
+        log.info("开始邮件解析 -> 邮箱信息:{},开始时间:{},结束时间:{}", mailboxInfoDTO, DateUtil.format(startDate,
+                DateConst.YYYY_MM_DD_HH_MM_SS), DateUtil.format(endDate, DateConst.YYYY_MM_DD_HH_MM_SS));
+        // 邮件类型配置
+        Map<Integer, List<String>> emailTypeMap = getEmailType();
+        // 邮件字段识别映射表
+//        Map<String, List<String>> emailFieldMap = getEmailFieldMapping();
+        Map<String, List<EmailContentInfoDTO>> emailContentMap;
+        try {
+            emailContentMap = realEmail(mailboxInfoDTO, emailTypeMap, startDate, endDate);
+        } catch (Exception e) {
+            log.info("采集邮件失败 -> 邮箱配置信息:{},堆栈信息:{}", mailboxInfoDTO, ExceptionUtil.stacktraceToString(e));
+            return;
+        }
+        if (MapUtil.isEmpty(emailContentMap)) {
+            log.info("未采集到邮件 -> 邮箱配置信息:{},开始时间:{},结束时间:{}", mailboxInfoDTO,
+                    DateUtil.format(startDate, DateConst.YYYY_MM_DD_HH_MM_SS), DateUtil.format(endDate, DateConst.YYYY_MM_DD_HH_MM_SS));
+            return;
+        }
+        for (Map.Entry<String, List<EmailContentInfoDTO>> emailEntry : emailContentMap.entrySet()) {
+            List<EmailContentInfoDTO> emailContentInfoDTOList = emailEntry.getValue();
+            if (CollUtil.isEmpty(emailContentInfoDTOList)) {
+                log.warn("未采集到正文或附件");
+                continue;
+            }
+            log.info("开始解析邮件数据 -> 邮件主题:{},邮件日期:{}", emailContentInfoDTOList.get(0).getEmailTitle(), emailContentInfoDTOList.get(0).getEmailDate());
+            Map<EmailContentInfoDTO, List<EmailZipFileDTO>> emailZipFileMap = MapUtil.newHashMap();
+            for (EmailContentInfoDTO emailContentInfoDTO : emailContentInfoDTOList) {
+                try {
+                    List<EmailZipFileDTO> fundNavDTOList = parseZipEmail(emailContentInfoDTO);
+                    emailZipFileMap.put(emailContentInfoDTO, fundNavDTOList);
+                } catch (Exception e) {
+                    log.error("堆栈信息:{}", ExceptionUtil.stacktraceToString(e));
+                }
+            }
+            // 保存相关信息 -> 邮件信息表,邮件文件表,邮件净值表,邮件规模表,基金净值表
+            saveRelatedTable(emailEntry.getKey(), mailboxInfoDTO.getAccount(), emailZipFileMap);
+            log.info("结束邮件解析 -> 邮箱信息:{},开始时间:{},结束时间:{}", mailboxInfoDTO,
+                    DateUtil.format(startDate, DateConst.YYYY_MM_DD_HH_MM_SS), DateUtil.format(endDate, DateConst.YYYY_MM_DD_HH_MM_SS));
+        }
+    }
+
+    public List<EmailZipFileDTO> parseZipEmail(EmailContentInfoDTO emailContentInfoDTO) {
+        List<EmailZipFileDTO> resultList = ListUtil.list(false);
+        Integer emailType = emailContentInfoDTO.getEmailType();
+        String filepath = emailContentInfoDTO.getFilePath();
+        if (ExcelUtil.isZip(filepath)) {
+            String destPath = filepath.replaceAll(".zip", "").replaceAll(".ZIP", "");
+            log.info("压缩包地址:{},解压后文件地址:{}", filepath, destPath);
+            List<String> dirs = ExcelUtil.extractCompressedFiles(filepath, destPath);
+            for (String dir : dirs) {
+                File file = new File(dir);
+                if (file.isDirectory() && file.list() != null) {
+                    for (String subDir : Objects.requireNonNull(file.list())) {
+                        resultList.add(new EmailZipFileDTO(subDir, emailType));
+                    }
+                } else {
+                    resultList.add(new EmailZipFileDTO(dir, emailType));
+                }
+            }
+        } else if (ExcelUtil.isRAR(filepath)) {
+            String destPath = filepath.replaceAll(".rar", "").replaceAll(".RAR", "");
+            File destFile = new File(destPath);
+            if (!destFile.exists()) {
+                destFile.mkdirs();
+            }
+            List<String> rarDirs = ExcelUtil.extractRar(filepath, destPath);
+            for (String rarDir : rarDirs) {
+                File file = new File(rarDir);
+                if (file.isDirectory() && file.list() != null) {
+                    for (String subRarDir : Objects.requireNonNull(file.list())) {
+                        resultList.add(new EmailZipFileDTO(subRarDir, emailType));
+                    }
+                } else {
+                    resultList.add(new EmailZipFileDTO(rarDir, emailType));
+                }
+            }
+        }
+        return resultList;
+    }
+
+    public void saveRelatedTable(String emailKey, String emailAddress,
+                                 Map<EmailContentInfoDTO, List<EmailZipFileDTO>> emailZipFileMap) {
+        // python 报告解析接口结果
+        List<ParseResult<ReportData>> dataList = ListUtil.list(false);
+        for (Map.Entry<EmailContentInfoDTO, List<EmailZipFileDTO>> entry : emailZipFileMap.entrySet()) {
+            EmailContentInfoDTO emailContentInfoDTO = entry.getKey();
+            Integer emailType = emailContentInfoDTO.getEmailType();
+            Integer emailId = emailContentInfoDTO.getEmailId();
+            EmailParseInfoDO emailParseInfoDO = buildEmailParseInfo(emailId, emailAddress, emailContentInfoDTO);
+            emailParseInfoDO.setEmailKey(emailKey);
+            emailId = saveEmailParseInfo(emailParseInfoDO);
+
+            List<EmailZipFileDTO> zipFiles = entry.getValue();
+            if (CollUtil.isNotEmpty(zipFiles)) {
+                for (EmailZipFileDTO zipFile : zipFiles) {
+                    EmailFileInfoDO emailFile = saveEmailFileInfo(emailId, null, zipFile.getFilename(), zipFile.getFilepath(), null);
+                    // 解析结果(可以从python获取或者自行解析)并保存报告
+                    ParseResult<ReportData> parseResult = this.parseReportAndHandleResult(emailFile.getId(), zipFile.getFilename(),
+                            zipFile.getFilepath(), emailType, emailFile.getAiFileId());
+                    dataList.add(parseResult);
+                }
+            } else {
+                String fileName = emailContentInfoDTO.getFileName();
+                EmailFileInfoDO emailFile = saveEmailFileInfo(emailId, emailContentInfoDTO.getFileId(), fileName,
+                        emailContentInfoDTO.getFilePath(), emailContentInfoDTO.getAiFileId());
+                // 解析结果(可以从python获取或者自行解析)并保存报告
+                ParseResult<ReportData> parseResult = this.parseReportAndHandleResult(emailFile.getId(), fileName,
+                        emailContentInfoDTO.getFilePath(), emailType, emailFile.getAiFileId());
+                dataList.add(parseResult);
+            }
+
+            String failReason = null;
+            int emailParseStatus = EmailParseStatusConst.SUCCESS;
+            // 报告邮件有一条失败就表示整个邮件解析失败
+            if (CollUtil.isNotEmpty(dataList)) {
+                // ai解析结果
+                List<ReportData> aiParaseList = dataList.stream().map(ParseResult::getData)
+                        .filter(Objects::nonNull).filter(e -> Objects.equals(true, e.getAiParse())).toList();
+                if (CollUtil.isNotEmpty(aiParaseList)) {
+                    for (ReportData data : aiParaseList) {
+                        this.emailFileInfoMapper.updateAiParseByFileId(data.getBaseInfo().getFileId(), data.getAiParse(), data.getAiFileId());
+                    }
+                }
+                long sucNum = dataList.stream().filter(e -> Objects.equals(1, e.getStatus())).count();
+                if (sucNum <= 0) {
+                    emailParseStatus = EmailParseStatusConst.FAIL;
+                    failReason = dataList.stream().map(ParseResult::getMsg).collect(Collectors.joining("/"));
+                }
+            }
+            emailParseInfoMapper.updateParseStatus(emailId, emailParseStatus, failReason);
+        }
+    }
+
+    private ParseResult<ReportData> parseReportAndHandleResult(int fileId, String fileName,
+                                                               String filepath, Integer emailType, String aiFileId) {
+        ParseResult<ReportData> result = new ParseResult<>();
+        if ((!Objects.equals(EmailTypeConst.REPORT_EMAIL_TYPE, emailType)
+                && !Objects.equals(EmailTypeConst.REPORT_LETTER_EMAIL_TYPE, emailType))
+                || StrUtil.isBlank(fileName)) {
+            result.setStatus(ReportParseStatus.NOT_A_REPORT.getCode());
+            result.setMsg(ReportParseStatus.NOT_A_REPORT.getMsg());
+            return result;
+        }
+        Pattern pattern = Pattern.compile("S(?:[A-Z]{0}[0-9]{5}|[A-Z][0-9]{4}|[A-Z]{2}[0-9]{3}|[A-Z]{3}[0-9]{2})");
+        Matcher matcher = pattern.matcher(fileName);
+        String registerNumber = null;
+        if (matcher.find()) {
+            registerNumber = matcher.group();
+        }
+        // 类型识别---先识别季度报告,没有季度再识别年度报告,最后识别月报
+        ReportType reportType = ReportParseUtils.matchReportType(fileName);
+        // 解析器--如果开启python解析则直接调用python接口,否则根据文件后缀获取对应解析器
+        ReportParserFileType fileType;
+        String fileSuffix = StrUtil.subAfter(fileName, ".", true);
+        fileType = ReportParserFileType.getBySuffix(fileSuffix);
+        // 不支持的格式
+        if (fileType == null) {
+            result.setStatus(ReportParseStatus.NO_SUPPORT_TEMPLATE.getCode());
+            result.setMsg(StrUtil.format(ReportParseStatus.NO_SUPPORT_TEMPLATE.getMsg(), fileName));
+            return result;
+        }
+        // 不是定期报告的判断逻辑放在不支持的格式下面
+        if (reportType == null) {
+            result.setStatus(ReportParseStatus.NOT_A_REPORT.getCode());
+            result.setMsg(StrUtil.format(ReportParseStatus.NOT_A_REPORT.getMsg(), fileName));
+            return result;
+        }
+        // 解析报告
+        ReportData reportData = null;
+        StopWatch parserWatch = new StopWatch();
+        parserWatch.start();
+        try {
+            ReportParserParams params = ReportParserParams.builder().fileId(fileId).filename(fileName)
+                    .filepath(filepath).registerNumber(registerNumber).reportType(reportType).aiFileId(aiFileId).build();
+            ReportParser<ReportData> instance = this.reportParserFactory.getInstance(reportType, fileType);
+            reportData = instance.parse(params);
+            result.setStatus(1);
+            result.setMsg("报告解析成功");
+            result.setData(reportData);
+        } catch (ReportParseException e) {
+            log.error("解析失败\n{}", e.getMsg());
+            result.setStatus(e.getCode());
+            result.setMsg(e.getMsg());
+        } catch (Exception e) {
+            log.error("解析错误\n{}", ExceptionUtil.stacktraceToString(e));
+            result.setStatus(ReportParseStatus.PARSE_FAIL.getCode());
+            result.setMsg(StrUtil.format(ReportParseStatus.PARSE_FAIL.getMsg(), e.getMessage()));
+        } finally {
+            parserWatch.stop();
+            if (log.isInfoEnabled()) {
+                log.info("报告{}解析结果为{},耗时{}ms", fileName, reportData, parserWatch.getTotalTimeMillis());
+            }
+        }
+        // 保存报告解析结果
+        if (reportData != null) {
+            StopWatch writeWatch = new StopWatch();
+            writeWatch.start();
+            try {
+                ReportWriter<ReportData> instance = this.reportWriterFactory.getInstance(reportType);
+                instance.write(reportData);
+            } catch (Exception e) {
+                log.error("报告{}结果保存失败\n{}", fileName, ExceptionUtil.stacktraceToString(e));
+            } finally {
+                writeWatch.stop();
+                if (log.isInfoEnabled()) {
+                    log.info("报告{}解析结果保存完成,耗时{}ms", fileName, writeWatch.getTotalTimeMillis());
+                }
+            }
+        }
+        return result;
+    }
+
+    private EmailFileInfoDO saveEmailFileInfo(Integer emailId, Integer fileId, String fileName, String filePath, String aiFileId) {
+        EmailFileInfoDO emailFileInfoDO = buildEmailFileInfoDO(emailId, fileId, fileName, filePath);
+        emailFileInfoDO.setAiFileId(aiFileId);
+        if (emailFileInfoDO.getId() != null) {
+            emailFileInfoMapper.updateTimeById(fileId, new Date());
+            return emailFileInfoDO;
+        }
+        emailFileInfoMapper.insert(emailFileInfoDO);
+        return emailFileInfoDO;
+    }
+
+    private EmailFileInfoDO buildEmailFileInfoDO(Integer emailId, Integer fileId, String fileName, String filePath) {
+        EmailFileInfoDO emailFileInfoDO = new EmailFileInfoDO();
+        emailFileInfoDO.setId(fileId);
+        emailFileInfoDO.setEmailId(emailId);
+        emailFileInfoDO.setFileName(fileName);
+        emailFileInfoDO.setFilePath(filePath);
+        emailFileInfoDO.setIsvalid(1);
+        emailFileInfoDO.setCreatorId(0);
+        emailFileInfoDO.setCreateTime(new Date());
+        emailFileInfoDO.setUpdaterId(0);
+        emailFileInfoDO.setUpdateTime(new Date());
+        return emailFileInfoDO;
+    }
+
+    private Integer saveEmailParseInfo(EmailParseInfoDO emailParseInfoDO) {
+        if (emailParseInfoDO == null) {
+            return null;
+        }
+        // 重新邮件功能 -> 修改解析时间和更新时间
+        if (emailParseInfoDO.getId() != null) {
+            emailParseInfoMapper.updateParseTime(emailParseInfoDO.getId(), emailParseInfoDO.getParseDate());
+            return emailParseInfoDO.getId();
+        }
+        emailParseInfoMapper.insert(emailParseInfoDO);
+        return emailParseInfoDO.getId();
+    }
+
+    private EmailParseInfoDO buildEmailParseInfo(Integer emailId, String emailAddress, EmailContentInfoDTO emailContentInfoDTO) {
+        EmailParseInfoDO emailParseInfoDO = new EmailParseInfoDO();
+        emailParseInfoDO.setId(emailId);
+        emailParseInfoDO.setSenderEmail(emailContentInfoDTO.getSenderEmail());
+        emailParseInfoDO.setEmail(emailAddress);
+        emailParseInfoDO.setEmailDate(DateUtil.parse(emailContentInfoDTO.getEmailDate(), DateConst.YYYY_MM_DD_HH_MM_SS));
+        emailParseInfoDO.setParseDate(emailParseInfoDO.getParseDate());
+        emailParseInfoDO.setEmailTitle(emailParseInfoDO.getEmailTitle());
+        emailParseInfoDO.setEmailType(emailContentInfoDTO.getEmailType());
+        emailParseInfoDO.setParseStatus(EmailParseStatusConst.SUCCESS);
+        emailParseInfoDO.setIsvalid(1);
+        emailParseInfoDO.setCreatorId(0);
+        emailParseInfoDO.setCreateTime(new Date());
+        emailParseInfoDO.setUpdaterId(0);
+        emailParseInfoDO.setUpdateTime(new Date());
+        return emailParseInfoDO;
+    }
+
+    public Map<Integer, List<String>> getEmailType() {
+        Map<Integer, List<String>> emailTypeMap = MapUtil.newHashMap(3, true);
+//        EmailTypeRuleDO emailTypeRuleDO = emailTypeRuleMapper.getEmailTypeRule();
+//        String nav = emailTypeRuleDO != null && StrUtil.isNotBlank(emailTypeRuleDO.getNav()) ? emailTypeRuleDO.getNav() : emailRuleConfig.getNav();
+//        String valuation = emailTypeRuleDO != null && StrUtil.isNotBlank(emailTypeRuleDO.getValuation()) ? emailTypeRuleDO.getValuation() : emailRuleConfig.getValuation();
+//        String report = emailTypeRuleDO != null && StrUtil.isNotBlank(emailTypeRuleDO.getReport()) ? emailTypeRuleDO.getReport() : emailRuleConfig.getReport();
+//        emailTypeMap.put(EmailTypeConst.VALUATION_EMAIL_TYPE, Arrays.stream(valuation.split(",")).toList());
+//        emailTypeMap.put(EmailTypeConst.NAV_EMAIL_TYPE, Arrays.stream(nav.split(",")).toList());
+        emailTypeMap.put(EmailTypeConst.REPORT_LETTER_EMAIL_TYPE, ListUtil.toList("确认单", "确认函", "确认"));
+        return emailTypeMap;
+    }
+
+    /**
+     * 读取邮件
+     *
+     * @param mailboxInfoDTO 邮箱配置信息
+     * @param emailTypeMap   邮件类型识别规则映射表
+     * @param startDate      邮件起始日期
+     * @param endDate        邮件截止日期(为null,将解析邮件日期小于等于startDate的当天邮件)
+     * @return 读取到的邮件信息
+     * @throws Exception 异常信息
+     */
+    private Map<String, List<EmailContentInfoDTO>> realEmail(MailboxInfoDTO mailboxInfoDTO,
+                                                             Map<Integer, List<String>> emailTypeMap, Date startDate, Date endDate) throws Exception {
+        Store store = EmailUtil.getStoreNew(mailboxInfoDTO);
+        if (store == null) {
+            return MapUtil.newHashMap();
+        }
+        // 默认读取收件箱的邮件
+        Folder folder = store.getFolder("INBOX");
+        folder.open(Folder.READ_ONLY);
+        Message[] messages = getEmailMessage(folder, mailboxInfoDTO.getProtocol(), startDate);
+        if (messages == null || messages.length == 0) {
+            log.info("获取不到邮件 -> 邮箱信息:{},开始时间:{},结束时间:{}", mailboxInfoDTO, startDate, endDate);
+            return MapUtil.newHashMap();
+        }
+        Map<String, List<EmailContentInfoDTO>> emailMessageMap = MapUtil.newHashMap();
+        for (Message message1 : messages) {
+            MimeMessage message = (MimeMessage) message1;
+            List<EmailContentInfoDTO> emailContentInfoDTOList = CollUtil.newArrayList();
+            String uuidKey = UUID.randomUUID().toString().replaceAll("-", "");
+            Integer emailType;
+            String senderEmail;
+            try {
+                Date emailDate = message.getSentDate();
+                boolean isNotParseConditionSatisfied = emailDate == null || (endDate != null && emailDate.compareTo(endDate) > 0) || (startDate != null && emailDate.compareTo(startDate) < 0);
+                if (isNotParseConditionSatisfied) {
+                    continue;
+                }
+                senderEmail = getSenderEmail(message);
+                emailType = EmailUtil.getEmailTypeBySubject(message.getSubject(), emailTypeMap);
+                String emailDateStr = DateUtil.format(emailDate, DateConst.YYYY_MM_DD_HH_MM_SS);
+                if (emailType == null) {
+                    log.info("邮件不满足解析条件 -> 邮件主题:{},邮件日期:{}", message.getSubject(), emailDateStr);
+                    continue;
+                }
+                log.info("邮件采集成功 -> 邮件主题:{},邮件日期:{}", message.getSubject(), emailDateStr);
+                Object content = message.getContent();
+                // 1.邮件为MIME多部分消息体:可能既有邮件又有正文
+                if (content instanceof MimeMultipart) {
+                    emailContentInfoDTOList = EmailUtil.collectMimeMultipart(message, mailboxInfoDTO.getAccount(), path);
+                }
+//                // 2.邮件只有正文
+//                if (content instanceof String) {
+//                    EmailContentInfoDTO emailContentInfoDTO = new EmailContentInfoDTO();
+//                    emailContentInfoDTO.setEmailContent(content.toString());
+//                    emailContentInfoDTO.setEmailDate(emailDateStr);
+//                    emailContentInfoDTO.setEmailTitle(message.getSubject());
+//                    String fileName = message.getSubject() + DateUtil.format(emailDate, DateConst.YYYYMMDDHHMMSS24);
+//                    String filePath = path + mailboxInfoDTO.getAccount() + "/" + DateUtil.format(emailDate, DateConst.YYYY_MM_DD) + "/" + fileName + ".html";
+//                    File saveFile = new File(filePath);
+//                    saveFile.setReadable(true);
+//                    if (!saveFile.exists()) {
+//                        if (!saveFile.getParentFile().exists()) {
+//                            saveFile.getParentFile().mkdirs();
+//                            saveFile.getParentFile().setExecutable(true);
+//                        }
+//                    }
+//                    FileUtil.writeFile(filePath, content.toString());
+//                    emailContentInfoDTO.setFilePath(filePath);
+//                    emailContentInfoDTO.setFileName(fileName);
+//                    emailContentInfoDTOList.add(emailContentInfoDTO);
+//                }
+                if (CollUtil.isNotEmpty(emailContentInfoDTOList)) {
+                    // 估值表或定期报告邮件不展示正文html文件
+                    if (emailType.equals(EmailTypeConst.VALUATION_EMAIL_TYPE) || emailType.equals(EmailTypeConst.REPORT_EMAIL_TYPE)) {
+                        emailContentInfoDTOList = emailContentInfoDTOList.stream().filter(e -> !ExcelUtil.isHTML(e.getFilePath())).toList();
+                    }
+                    emailContentInfoDTOList.forEach(e -> {
+                        e.setEmailType(emailType);
+                        e.setSenderEmail(senderEmail);
+                    });
+                    emailMessageMap.put(uuidKey, emailContentInfoDTOList);
+                }
+            } catch (Exception e) {
+                log.error("获取邮箱的邮件报错,堆栈信息:{}", ExceptionUtil.stacktraceToString(e));
+            }
+        }
+        folder.close(false);
+        store.close();
+        return emailMessageMap;
+    }
+
+    private String getSenderEmail(MimeMessage message) {
+        Address[] senderAddress = null;
+        try {
+            senderAddress = message.getFrom();
+            if (senderAddress == null || senderAddress.length == 0) {
+                log.info("发件人获取失败=============================");
+                return null;
+            }
+            // 此时的address是含有编码(MIME编码方式)后的文本和实际的邮件地址
+            String address = "";
+            for (Address from : senderAddress) {
+                if (StrUtil.isNotBlank(from.toString())) {
+                    address = from.toString();
+                    break;
+                }
+            }
+            log.info("发件人地址:" + address + "========================senderAddress size:" + senderAddress.length);
+            // 正则表达式匹配邮件地址
+            Pattern pattern = Pattern.compile("<(\\S+)>");
+            Matcher matcher = pattern.matcher(address);
+            if (matcher.find()) {
+                return matcher.group(1);
+            }
+            //说明匹配不到,直接获取sender
+            Address sender = message.getSender();
+            if (sender == null) {
+                return address;
+            }
+            String senderEmail = sender.toString();
+            log.info("senderEmail:" + senderEmail + "====================");
+            if (senderEmail.contains("<") && senderEmail.contains(">") && senderEmail.indexOf("<") < senderEmail.indexOf(">")) {
+                senderEmail = senderEmail.substring(senderEmail.indexOf("<") + 1, senderEmail.length() - 1);
+            }
+            return senderEmail;
+        } catch (MessagingException e) {
+            log.error(e.getMessage(), e);
+        }
+        return null;
+    }
+
+    private Message[] getEmailMessage(Folder folder, String protocol, Date startDate) {
+        try {
+            if (protocol.contains("imap")) {
+                // 获取邮件日期大于等于startDate的邮件(搜索条件只支持按天)
+                SearchTerm startDateTerm = new ReceivedDateTerm(ComparisonTerm.GE, startDate);
+                return folder.search(startDateTerm);
+            } else {
+                return folder.getMessages();
+            }
+        } catch (MessagingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}

+ 0 - 0
mo-daq/src/main/java/com/smppw/modaq/infrastructure/config/DataSourceAutoConfig.java


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio