Compare commits

...

89 Commits

Author SHA1 Message Date
9c929301b9 feat(credit): 添加客户跟进七步骤功能
- 在CreditMpCustomer实体类中添加七个跟进步骤相关字段
- 实现跟进步骤审核功能,包括单个和批量审核接口
- 添加待审核步骤查询接口和客户跟进统计功能
- 实现流程结束功能和详细的步骤状态管理
- 创建相应的DTO和VO数据传输对象
- 在Mapper层添加待审核步骤查询SQL实现
2026-03-22 22:32:02 +08:00
ddebb6f16c feat(order): 添加配送员端可接单查询接口并修复权限控制
- 新增 /rider/available 接口供配送员查询可接单的送水订单
- 实现未分配订单查询逻辑(riderId 为 null 或 0 的订单)
- 修复配送员端订单查询权限控制,确保只能查看分配给自己的订单
- 优化 SQL 查询条件,支持通过 riderId 参数灵活控制订单分配状态筛选
- 添加配送员身份验证和租户隔离机制
- 设置配送员端默认排序规则为按期望配送时间优先
2026-03-22 09:48:38 +08:00
aea60e330d feat(entity): 添加客户实体真实姓名字段
- 在CreditMpCustomer实体中新增realName属性
- 为realName字段添加Schema注解描述
- 在关联查询SQL中加入real_name字段映射
- 更新表字段映射配置支持新字段
2026-03-19 17:37:21 +08:00
77a276e643 feat(credit): 添加客户流程步骤字段支持
- 在CreditMpCustomer实体中新增step字段用于跟踪客户流程步骤
- 更新status字段描述从"0正常, 1冻结"改为"0未受理, 1已受理"
- 在CreditMpCustomerMapper.xml中添加step查询条件支持
- 在CreditMpCustomerParam参数类中添加step查询字段
- 定义步骤状态:0未受理1已受理2材料提交3合同签订4执行回款5完结
2026-03-19 16:13:03 +08:00
3b723a74c6 perf(database): 优化数据库配置和用户查询性能
- 更新 Redis 配置为本地 1Panel 实例
- 为 UserMapper 查询添加 LIMIT 1 限制
- 为 credit_user 表创建多个复合索引以提升查询效率
- 添加租户、公司、用户维度的索引支持
- 优化默认分页排序的索引覆盖
- 包含统计关联数的索引优化
2026-03-19 00:00:45 +08:00
6a48722b67 fix(database): 修复用户角色查询中的表名引用问题
- 修正了 UserMapper.xml 中 sys_user_role 表的数据库模式前缀
- 添加了正确的 gxwebsoft_core 模式前缀以确保跨数据库兼容性
- 解决了由于表名解析错误可能导致的查询失败问题
2026-03-18 10:57:02 +08:00
ced6178271 feat(wx-login): 更新微信登录配置和优化access_token管理
- 修改application.yml中的激活环境从glt2到ysb2
- 在CreditMpCustomerMapper.xml中增加to_user字段的关键词搜索功能
- 删除WxLoginController中重复的TimeUnit导入
- 实现access_token失效时的自动刷新重试机制
- 使用微信稳定的stable_token接口替代原有token接口
- 添加强制刷新access_token的功能支持
- 优化缓存TTL计算逻辑,确保token在过期前及时刷新
- 改进微信API错误处理和异常信息提示
2026-03-18 10:43:10 +08:00
5775ad862d config(env): 更新环境配置和数据库连接信息
- 将默认激活环境从 ysb2 切换到 glt2
- 更新 glt2 环境的数据源配置,包括数据库名称、用户名和密码
- 修改 glt3 环境的 Redis 配置,更新数据库编号、主机地址、端口和密码
- 将微信登录控制器中的默认租户ID从 10550 更改为 10584
2026-03-17 11:03:14 +08:00
04d82682de fix(database): 修复客户表与用户表关联查询的字段映射错误
- 修正了 LEFT JOIN 条件中的字段引用错误
- 将 u.id = a.user_id 更改为 u.user_id = a.user_id
- 确保了用户信息能够正确关联到客户记录上
2026-03-16 23:36:48 +08:00
41fb24b9ff feat(customer): 添加客户实体用户关联字段支持
- 修改application.yml配置文件激活环境从glt2切换到ysb2
- 在CreditMpCustomer实体类中新增nickname、phone、avatar三个关联字段
- 为新增字段添加TableField注解标记为非数据库字段
- 更新CreditMpCustomerMapper.xml中的关联查询SQL
- 添加LEFT JOIN子句关联sys_user表获取用户信息
- 查询结果中包含用户昵称、头像、手机号等扩展信息
2026-03-16 22:32:30 +08:00
044d87cfde feat(credit): 新增小程序端客户管理功能
- 修改Redis数据库配置从database 0切换到database 3
- 添加代码生成工具YsbGenerator用于自动生成CRUD代码
- 生成CreditMpCustomer实体类包含客户基本信息和状态字段
- 创建CreditMpCustomerController提供RESTful API接口
- 实现CreditMpCustomerService业务逻辑层
- 添加CreditMpCustomerMapper数据访问层及XML映射文件
- 生成CreditMpCustomerParam查询参数类支持条件筛选
- 实现服务层接口及具体业务逻辑方法
- 集成权限控制和操作日志记录功能
- 支持分页查询、批量操作和关联查询功能
2026-03-16 21:14:36 +08:00
7fe347d7bc feat(shop): 更新数据库配置并添加订单状态关联查询
- 修改 application-glt2.yml 中的数据源配置,将数据库从 modules 更改为 gltdb
- 修改 application-glt3.yml 中的数据源配置,将数据库从 modules 更改为 gltdb
- 在 ShopDealerCapital 实体类中添加 orderStatus 字段用于存储订单状态
- 更新 ShopDealerCapitalMapper.xml 查询语句,关联 shop_order 表获取订单状态
- 添加 LEFT JOIN 条件连接 shop_order 表以获取订单状态信息
2026-03-16 13:10:26 +08:00
3f546f7e70 feat(shop): 更新数据库配置并添加订单状态关联查询
- 修改 application-glt2.yml 中的数据源配置,将数据库从 modules 更改为 gltdb
- 修改 application-glt3.yml 中的数据源配置,将数据库从 modules 更改为 gltdb
- 在 ShopDealerCapital 实体类中添加 orderStatus 字段用于存储订单状态
- 更新 ShopDealerCapitalMapper.xml 查询语句,关联 shop_order 表获取订单状态
- 添加 LEFT JOIN 条件连接 shop_order 表以获取订单状态信息
2026-03-16 12:50:52 +08:00
896491fa0b Merge remote-tracking branch 'origin/master' 2026-03-16 00:21:11 +08:00
9c85223545 feat(order): 更新订单修改功能并添加新环境配置
- 修改 application.yml 默认激活环境从 ysb2 到 glt2
- 新增 application-glt3.yml 环境配置文件,包含服务器、数据源、Redis等完整配置
- 在订单更新接口中添加地址同步逻辑,支持根据 addressId 更新订单地址快照
- 添加订单更新时的参数验证和权限检查机制
- 在订单模板实体中新增步长字段用于业务配置
- 优化订单更新流程中的租户和用户权限验证逻辑
2026-03-16 00:21:05 +08:00
f783aaa242 feat(entity): 添加企业别名字段到多个实体类
- 在 CreditAdministrativeLicense 实体中添加 companyAlias 字段
- 在 CreditBranch 实体中添加 companyAlias 字段
- 在 CreditCompetitor 实体中添加 companyAlias 字段
- 在 CreditCustomer 实体中添加 companyAlias 字段
- 在 CreditExternal 实体中添加 companyAlias 字段
- 在 CreditHistoricalLegalPerson 实体中添加 companyAlias 字段
- 在 CreditNearbyCompany 实体中添加 companyAlias 字段
- 在 CreditRiskRelation 实体中添加 companyAlias 字段
- 在 CreditSupplier 实体中添加 companyAlias 字段
- 在 CreditSuspectedRelationship 实体中添加 companyAlias 字段
- 在 CreditUser 实体中添加 companyAlias 字段
2026-03-15 14:06:31 +08:00
50f6b49da9 fix(import): 修复Excel导入时企业名称字段处理问题
- 移除CreditCompetitor、CreditCustomer、CreditExternal、CreditRiskRelation、CreditSupplier和CreditUser实体中companyName字段的@TableField(exist = false)注解
- 在各控制器的convertImportParamToEntity方法中添加CreditCompany::setCompanyName方法引用
- 从companyId获取对应的企业名称并设置到fixedCompanyName变量中
- 更新导入逻辑中的注释说明,明确name和companyName字段的不同用途
- 在导入过程中当companyName为空且存在固定公司名称时进行赋值处理
2026-03-15 11:55:24 +08:00
7c1af7c207 feat(credit): 添加公司客户标识字段支持查询过滤
- 在CreditCompany实体中新增isCustomer字段
- 在CreditCompanyParam参数类中添加isCustomer查询参数
- 在CreditCompanyMapper.xml中增加isCustomer条件过滤
- 修改application.yml中的默认激活环境配置
2026-03-15 11:36:28 +08:00
b827052956 feat(settlement): 优化经销商订单结算逻辑支持水票模板订单
- 引入水票模板服务和相关实体类
- 添加水票模板商品ID加载功能
- 修改订单查询逻辑支持水票模板订单即时结算
- 更新订单认领逻辑适配水票模板订单条件
- 实现普通订单和水票模板订单差异化结算规则
- 添加异常处理确保系统稳定性
2026-03-11 15:14:15 +08:00
2a05686b75 fix(controller): 移除水票模板控制器中多余的权限验证注解
- 移除了根据id查询水票接口的@PreAuthorize权限验证注解
- 保留了原有的操作描述和请求映射配置
- 简化了接口的安全配置,使查询功能更易于访问
2026-03-11 13:37:47 +08:00
233af57bad refactor(glt-ticket): 调整套票发放逻辑,移除自动核销功能
- 修改套票发放任务注释,明确发放阶段不再自动核销/自动下单
- 移除起始送水自动核销相关代码和常量定义
- 删除自动核销相关的服务依赖注入
- 更新套票发放逻辑,按整改需求仅记录startSendQty配置但不执行自动核销
- 移除构建起始送水订单的相关方法
- 添加送水时间格式化常量用于立刻送水场景
- 实现立刻送水时自动设置当前时间为配送时间的功能
2026-03-10 13:04:22 +08:00
039508412b refactor(glt-ticket): 调整套票发放逻辑,移除自动核销功能
- 修改套票发放任务注释,明确发放阶段不再自动核销/自动下单
- 移除起始送水自动核销相关代码和常量定义
- 删除自动核销相关的服务依赖注入
- 更新套票发放逻辑,按整改需求仅记录startSendQty配置但不执行自动核销
- 移除构建起始送水订单的相关方法
- 添加送水时间格式化常量用于立刻送水场景
- 实现立刻送水时自动设置当前时间为配送时间的功能
2026-03-09 16:54:17 +08:00
26920cbbe3 config(core): 更新应用配置和定时任务调度
- 修改默认激活环境从 ysb2 到 glt2
- 调整经销商佣金解冻任务执行频率从 30 秒到 20 秒
- 修改订单自动取消任务执行频率从 5 分钟到 1 分钟
2026-03-09 12:56:17 +08:00
93dbc22603 refactor(task): 统一佣金分红术语为分润
- 将"分红"相关术语统一替换为"分润",包括日志信息中的"未找到分红账号"改为"未找到分润账号"
- 修改注释中"门店分红上级"为"门店分润上级","总经销商分润"为"分润"
- 更新佣金计算相关注释和日志信息,将"直推佣金"、"门店直推佣金"等统一为"分佣"、"门店直推分润"
- 修正ShopDealerOrder数据回填逻辑中的术语表述,将"门店分红字段"改为"门店分润字段"
- 调整门店分润规则注释和订单记录落字段说明,统一使用分润概念
2026-03-07 01:58:28 +08:00
43a98cf7cd feat(shop): 更新应用配置和商店搜索功能
- 修改日志级别配置,将com.gxwebsoft和mybatis-plus设置为DEBUG模式
- 移除文件日志配置和root日志级别设置
- 更新商店搜索条件,从comments字段改为name和phone字段搜索
- 添加OR条件支持电话号码搜索功能
2026-03-06 16:59:16 +08:00
b1cd1cff7e refactor(credit): 重构企业实体字段定义和导入参数配置
- 将mailingEmail字段描述从"通信地址邮箱"更正为"通信地址邮编"
- 移除CreditNearbyCompany实体中多个冗余字段的数据库映射注解
- 在控制器中移除实缴资本等字段的赋值逻辑
- 更新导入参数类中对应字段的Excel名称标注
- 添加更多联系方式字段的支持
2026-03-03 16:32:30 +08:00
d69481f4c3 feat(param): 添加企业导入参数的Excel注解支持
- 为纳税人识别号字段添加Excel注解
- 为注册号字段添加Excel注解
- 为组织机构代码字段添加Excel注解
- 为参保人数字段添加Excel注解
- 为参保人数所属年报字段添加Excel注解
- 为营业期限字段添加Excel注解
- 为国标行业门类字段添加Excel注解
- 为国标行业大类字段添加Excel注解
- 为国标行业中类字段添加Excel注解
- 为国标行业小类字段添加Excel注解
- 为曾用名字段添加Excel注解
- 为英文名字段添加Excel注解
- 为通信地址字段添加Excel注解
- 为通信地址邮箱字段添加Excel注解
- 为注册地址邮编字段添加Excel注解
- 为电话字段添加Excel注解
- 新增更多电话字段并添加Excel注解
- 为企查查行业分类字段添加Excel注解
- 为类型字段添加Excel注解
- 为登记机关字段添加Excel注解
- 为纳税人资质字段添加Excel注解
- 为最新年报年份字段添加Excel注解
- 为最新年报营业收入字段添加Excel注解
- 为企业信用相关字段添加Excel注解
2026-03-03 16:17:31 +08:00
b9fd76f855 feat(param): 添加企业导入参数的Excel注解支持
- 为纳税人识别号字段添加Excel注解
- 为注册号字段添加Excel注解
- 为组织机构代码字段添加Excel注解
- 为参保人数字段添加Excel注解
- 为参保人数所属年报字段添加Excel注解
- 为营业期限字段添加Excel注解
- 为国标行业门类字段添加Excel注解
- 为国标行业大类字段添加Excel注解
- 为国标行业中类字段添加Excel注解
- 为国标行业小类字段添加Excel注解
- 为曾用名字段添加Excel注解
- 为英文名字段添加Excel注解
- 为通信地址字段添加Excel注解
- 为通信地址邮箱字段添加Excel注解
- 为注册
2026-03-03 16:12:05 +08:00
12877c7b8e fix(data-import): 修复数据导入中的字段映射和标注问题
- 修正CreditGqdjController中历史数据标记字段从dataStatus改为dataType
- 修正CreditGqdjImportParam中Excel字段标注将"类型"改为"数据状态"
- 为CreditNearbyCompanyImportParam添加邮政编码字段postalCode
- 在CreditNearbyCompanyController中增加邮政编码字段设置逻辑
- 为CreditXgxfImportParam添加ExcelHeaderAlias注解支持多字段别名映射
2026-03-03 16:09:37 +08:00
a4dbe758e3 feat(credit): 添加原告/上诉人字段支持
- 在CreditUser实体中新增plaintiffAppellant字段用于存储原告/上诉人信息
- 更新CreditUserController中的数据映射逻辑以包含新字段
- 在CreditUserImportParam中添加Excel导入支持和字段映射
- 在CreditUserParam中定义查询参数结构包含新字段
- 新增CreditBankruptcyImportSheetSelectionTest测试类验证多工作表选择逻辑
- 实现破产重整数据导入时优先选择正确工作表的功能
2026-03-03 15:02:05 +08:00
f016acda91 feat(credit): 优化破产重整数据导入功能
- 优先从名为"破产重整"的标签页导入数据,避免多工作表文件中的意外导入
- 当指定标签页不存在时,向后兼容使用任意工作表导入方式
- 添加详细的注释说明导入逻辑和向后兼容性处理
2026-03-03 14:41:46 +08:00
808ac75253 feat(ticket): 实现水票分期释放功能支持
- 新增 releasePeriods 配置支持,可按总数量分期平均释放
- 修改原有逻辑:购买量立即可用,赠送量冻结按计划释放
- 当配置期数时按总票数生成每期释放计划,否则保持原逻辑
- 支持首期释放时机控制,支付成功当刻可立即释放首期票数
- 更新水票释放计划生成逻辑,期号从0开始计数
- 修正水票日志记录中的数量统计逻辑
2026-03-02 17:32:21 +08:00
521de8509b feat(credit): 添加历史数据批量导入功能
- 在CreditCaseFilingController中新增批量导入历史立案信息接口
- 在CreditDeliveryNoticeController中新增批量导入历史送达公告接口
- 在CreditMediationController中新增批量导入历史诉前调解接口
- 实现Excel文件解析和数据验证逻辑
- 添加数据库唯一索引约束防止重复数据导入
- 统一将历史导入数据标记为"失效"状态
- 集成权限控制和用户信息自动填充
- 实现分块处理提高大批量数据导入性能
- 添加错误消息收集和返回机制
2026-03-02 14:55:47 +08:00
5fd87bbb1c feat(credit): 更新企业信用模块配置和实体字段
- 修改 application-ysb2.yml 配置文件注释从生产环境改为服务器配置
- 更新 CreditCourtAnnouncement 实体中 plaintiffAppellant 字段描述为原告/上诉人
- 调整 CreditCourtAnnouncementImportParam 中 Excel 注解字段名称和别名配置
- 在 CreditNearbyCompanyController 中添加多个公司信息字段映射包括纳税人识别号、注册号等
- 扩展 CreditNearbyCompanyImportParam 类增加数十个公司相关字段和 Schema 注解
- 移除无用的日志文件 websoft-modules.log.2026-02-24.0.gz
2026-03-02 14:47:41 +08:00
cd0433c86b fix(excel): 修复法院名称字段的Excel表头别名配置
- 为courtName字段添加了缺失的"法院"表头别名
- 确保Excel导入时能够正确识别法院列的数据
2026-03-02 13:18:23 +08:00
22c1f42394 chore(config): 更新应用配置和实体注解
- 将应用活跃配置从 glt2 更改为 ysb2
- 为 GltTicketOrder 实体的 orderNo 字段添加 TableField 注解
- 更新 README 文件格式,添加空行结尾
2026-03-02 13:10:55 +08:00
f7334021e0 feat(ticket): 添加订单状态字段并完善水票订单关联逻辑
- 在GltTicketOrder实体中添加orderStatus字段用于存储订单状态
- 更新GltTicketOrderMapper.xml查询语句以包含orderStatus字段
- 在GltTicketOrderParam参数类中添加orderStatus查询条件支持
- 实现resolveShopOrderNo方法用于关联商城订单号的获取逻辑
- 添加测试方法testOrderData用于处理已退款订单相关的水票撤销操作
- 增加findTicketsByOrder和findTicketsByOrderGoodsFallback辅助方法
- 完善水票订单与商城订单的关联关系处理机制
2026-03-01 21:12:14 +08:00
1c1c341bb9 feat(ticket): 添加订单状态查询功能
- 移除 GltTicketOrder 中 orderNo 字段的 @TableField(exist = false) 注解
- 在 GltTicketOrderParam 中添加 orderNo 字段并导入 TableField 注解
- 在 GltUserTicket 中添加 orderStatus 字段用于存储订单状态
- 更新 GltUserTicketMapper.xml 中的关联查询 SQL,添加订单状态字段映射
- 修改关联条件从 order_id 改为 order_no 进行关联查询
- 在查询条件中添加订单状态的筛选功能
- 在 GltUserTicketParam 中添加 orderStatus 查询参数
2026-03-01 20:44:46 +08:00
4dae378c9a feat(shop): 添加订单取消和退款时的水票撤销功能
- 在ShopOrderController中注入GltTicketRevokeService服务
- 实现订单状态改为已取消时同步撤销相关水票、释放计划和送水订单
- 实现退款成功后自动撤销水票相关数据的功能
- 新增GltTicketRevokeService服务处理水票撤销逻辑
- 添加批量订单取消时的水票撤销支持
- 实现撤销操作的幂等性确保无副作用
- 添加单元测试验证水票撤销功能的正确性
2026-03-01 00:43:28 +08:00
a8af20bcde feat(shop): 添加订单取消和退款时的水票撤销功能
- 在ShopOrderController中注入GltTicketRevokeService服务
- 实现订单状态改为已取消时同步撤销相关水票、释放计划和送水订单
- 实现退款成功后自动撤销水票相关数据的功能
- 新增GltTicketRevokeService服务处理水票撤销逻辑
- 添加批量订单取消时的水票撤销支持
- 实现撤销操作的幂等性确保无副作用
- 添加单元测试验证水票撤销功能的正确性
2026-03-01 00:30:21 +08:00
2044bdc87a feat(order): 添加订单状态字段并更新关联查询
- 在 ShopDealerOrder 实体中新增 orderStatus 字段用于显示订单状态
- 更新 ShopDealerOrderMapper.xml 中的关联查询,加入订单状态字段映射
- 修改 application.yml 配置文件,将默认激活环境从 dev 改为 glt2
- 通过 LEFT JOIN 关联 shop_order 表获取订单状态数据
2026-02-28 20:17:01 +08:00
1c78fdbef4 feat(ai): 新增AI模块功能
- 添加Ollama配置参数,包括基础URL、模型设置、超时配置等
- 创建AI知识库相关数据库表(文档表和分段表)
- 实现AI数据分析服务,支持订单数据查询和分析
- 开发AI聊天控制器,提供模型列表、对话和流式对话功能
- 构建知识库RAG服务,支持文档上传、CMS同步和问答功能
- 添加多种AI相关的DTO类和实体类
- 实现AI嵌入向量计算和相似度匹配算法
- 集成Tika用于文档内容提取和解析
2026-02-28 08:30:48 +08:00
3cadaab214 fix(ticket): 解决订单状态更新和并发处理问题
- 在订单完成后添加状态检查和更新,避免重复处理
- 增加行锁机制防止多实例并发执行导致的重复发放
- 对票券模板查询添加确定性排序确保一致性
- 添加详细日志记录起始送水核销和套票发放过程
- 优化查询条件防止重复扫描已处理订单
2026-02-27 22:06:19 +08:00
ecbe4fbaea fix(excel): 解决Excel导入时字段映射和数据处理问题
- 为CreditCourtAnnouncementImportParam添加ExcelHeaderAlias注解支持多别名映射
- 为CreditJudgmentDebtorImportParam添加ExcelHeaderAlias注解支持多别名映射
- 修复CreditXgxfController中appellee字段取值逻辑,使用正确的数据源
- 统一字段映射规则,确保Excel表头别名能够正确识别
2026-02-27 18:28:26 +08:00
af5a0d352e fix(mapper): 修改SQL查询逻辑以优化公司名称显示
- 将COALESCE函数替换为直接使用关联表的match_name字段
- 移除对原表company_name字段的回退逻辑
- 统一四个映射文件中的SQL查询结构
- 确保所有关联查询都使用一致的字段映射方式
2026-02-27 17:14:05 +08:00
5cc9219801 refactor(credit): 优化Excel导入功能并改进表头匹配
- 移除过时的Excel导入相关依赖和方法
- 使用ExcelImportSupport替代原有的多配置尝试导入方式
- 添加表头文本标准化处理以解决空白字符导致的匹配问题
- 引入ExcelHeaderAlias注解支持表头别名匹配
- 简化导入逻辑并提高表头识别准确性
2026-02-27 16:14:08 +08:00
5f6a8ab089 feat(excel): 添加Excel导入表头别名支持功能
- 新增ExcelHeaderAlias注解用于定义表头别名
- 在CreditExternalImportParam中为认缴出资额字段添加别名映射
- 扩展ExcelImportSupport类支持运行时表头别名解析
- 实现别名到标准表头名称的规范化映射逻辑
- 保持导出模板表头不变的情况下支持多种导入表头格式
2026-02-27 15:23:59 +08:00
102a45ef3a feat(entity): 添加公司名称字段的表映射配置
- 在CreditAdministrativeLicense实体中为companyName字段添加@TableField(exist = false)注解
- 在CreditBranch实体中为companyName字段添加@TableField(exist = false)注解
- 在CreditHistoricalLegalPerson实体中为companyName字段添加@TableField(exist = false)注解
- 在CreditNearbyCompany实体中为companyName字段添加@TableField(exist = false)注解
- 在CreditSuspectedRelationship实体中为companyName字段添加@TableField(exist = false)注解
2026-02-27 13:19:56 +08:00
9b18851aaf fix(credit): 修复公司名称字段映射问题
- 移除实体类中的多余 TableField 注解
- 修改 SQL 查询逻辑,使用 match_name 字段替代原来的 company_name
- 统一多个信用相关实体的字段映射方式
- 添加新的生产环境配置文件 application-ysb2.yml
- 更新默认激活的环境配置为 ysb2
2026-02-27 12:34:01 +08:00
f25e2c3707 feat(withdraw): 优化经销商提现审核流程
- 修改注释说明,默认进入待审核状态,部分租户小额提现可自动审核
- 调整租户ID比较逻辑,使用Integer.valueOf进行包装比较
- 优化小额提现自动审核条件,金额小于100自动通过,否则进入待审核
- 添加不同金额段的审核人员配置说明
2026-02-25 15:08:57 +08:00
ca02e9e5a3 feat(glt): 添加送水订单自动派单功能
- 在ShopStoreRider实体中增加经纬度字段用于定位
- 创建GltTicketOrderAutoDispatch10584Task定时任务处理自动派单
- 实现GltTicketOrderAutoDispatchService服务类进行距离计算和派单逻辑
- 支持按距离最近原则自动分配配送员给待配送订单
- 集成坐标解析和Haversine距离计算算法
- 实现多租户环境下的自动派单配置开关
- 添加配送员在途订单数限制和并发控制机制
2026-02-25 13:45:16 +08:00
409a078e2d feat(batch-import): 批量导入功能支持企业名称回填
- 在 BatchImportSupport 中新增 refreshCompanyIdByCompanyName 方法用于按企业名称匹配并回填 companyId 和 companyName
- 在 CreditAdministrativeLicenseController、CreditBranchController、CreditHistoricalLegalPersonController、CreditNearbyCompanyController 和 CreditSuspectedRelationshipController 中添加公司名称回填逻辑
- 修改实体类中 companyName 字段的 TableField 注解从 exist=false 改为 company_name
- 更新各个 Mapper XML 文件中的查询 SQL,使用 COALESCE 函数确保当关联企业名称为空时使用本地存储的公司名称
- 在批量导入过程中增加固定公司名称的获取和设置逻辑
2026-02-25 12:59:50 +08:00
34554cbaac fix(payment): 解决微信支付异常处理和SQL Runner配置问题
- 在application.yml中启用enable-sql-runner配置以解决SqlRunner删除操作报错
- 在application-ysb.yml中补充完整的mybatis-plus配置包括SQL Runner支持
- 修改ShopDealerWithdrawController中openid获取逻辑,统一使用小程序openid避免微信400错误
- 更新ShopDealerWithdrawMapper.xml中字段别名映射确保数据正确显示
- 在WxTransferService中增强ServiceException处理,提供更友好的错误信息
- 添加详细的异常转换方法toPaymentException解析微信API错误详情
- 补充必要的Gson依赖导入处理JSON响应数据
2026-02-24 19:30:18 +08:00
fe893c71f6 config(server): 配置生产环境服务器端口
- 在 application-ysb.yml 中添加 server.port 配置
- 设置生产环境服务器端口为 9300
2026-02-24 17:27:15 +08:00
bb45d4b96e ```
fix(task): 修复配送奖励计算逻辑并兼容多种录入方式

- 添加normalizeDeliveryRate方法处理配送费率兼容性问题
- 支持0.05表示5%(比例)和5表示5%(百分比)两种录入方式
- 移除查询条件中不必要的userId限制
- 重构奖励计算逻辑使用标准化后的费率进行计算
- 添加单价验证确保只有有效价格参与计算
- 优化订单行金额计算确保精确度
```
2026-02-24 16:40:40 +08:00
e7ba0626cd Merge remote-tracking branch 'origin/master' 2026-02-24 16:11:26 +08:00
87f38be98a feat(task): 新增配送奖励功能
- 在 DealerCommissionUnfreeze10584Task 中新增配送奖励结算逻辑
- 引入 ShopGoods 和 ShopOrderGoods 实体及相关服务依赖
- 添加 FLOW_TYPE_DELIVERY_REWARD 常量用于标识配送奖励流水类型
- 实现 settleDeliveryRewardIfNeeded 方法处理配送奖励计算和发放
- 增加配送奖励幂等性检查避免重复发放
- 更新 ShopDealerCapital 实体的资金流动类型描述,添加配送奖励类型
- 在 ShopGoods 实体中增加 deliveryMoney 字段用于存储配送奖金配置
- 实现配送奖励的事务性处理和并发安全控制
2026-02-24 16:11:02 +08:00
6e04ca07bb config(database): 更新数据库和Redis连接配置
- 修改MySQL数据库主机地址从1Panel-mysql-XsWW到1Panel-mysql-Bqdt
- 修改Redis服务器地址从1Panel-redis-GmNr到1Panel-redis-Q1LE
- 更新Redis密码从redis_t74P8C到redis_WSDb88
2026-02-24 16:09:34 +08:00
0fb8c01140 feat(config): 添加生产环境配置文件
- 配置MySQL数据库连接信息,包括URL、用户名和密码
- 设置Redis缓存服务器连接参数
- 配置Socket.IO服务器主机地址
- 添加MQTT消息队列服务相关配置
- 设置文件服务器和API接口URL路径
- 配置阿里云OSS对象存储服务参数
- 添加证书加载模式和路径配置
- 配置支付缓存键前缀和过期时间
- 设置阿里云翻译服务访问凭证
- 添加微信支付转账场景配置
2026-02-24 15:59:59 +08:00
429f3e1e8e feat(config): 添加新的glt2环境配置并调整订单状态逻辑
- 新增 application-glt2.yml 环境配置文件
- 将默认激活环境从 dev 切换到 glt2
- 更新数据库连接地址和 Redis 配置
- 调整订单退款状态验证逻辑
- 修改订单状态判断条件以支持退款流程优化
2026-02-24 13:09:55 +08:00
ae24a9a99e fix(withdraw): 更新提现自动审核金额阈值
- 将自动审核通过的金额阈值从50调整为100
- 相应更新人工审核的起始金额条件
- 保持原有的多级审核流程逻辑不变
2026-02-24 12:10:43 +08:00
a5f18859dc feat(payment): 优化支付回调地址配置逻辑
- 开发测试环境支持强制覆盖回调地址,便于本地联调
- 生产环境优先使用数据库中的notifyUrl配置
- 数据库未配置时才使用环境默认配置作为兜底方案
- 实现从通知URL提取基础API URL的功能
- 重构默认回调URL生成逻辑,优先使用数据库配置
- 移除重复的回调地址添加,优化地址列表构建流程
2026-02-24 12:05:10 +08:00
8bafc724a4 feat(config): 添加API网关配置并更新订单通知URL
- 在配置属性中新增apiUrl字段用于API网关地址
- 为开发、测试、生产环境配置文件添加api-url配置项
- 更新商城订单服务中的通知URL使用API网关地址
- 修改默认订单通知URL和微信支付通知URL的获取逻辑
- 保留server-url作为基础模块接口,api-url作为业务模块接口
- 实现基础模块和业务模块接口分离的架构设计
2026-02-24 11:24:26 +08:00
9590227004 fix(payment): 解决微信支付成功时间解析问题
- 在ShopOrderServiceImpl和WxPayNotifyService中添加OffsetDateTime导入
- 修改支付成功时间处理逻辑,从微信返回的RFC3339格式时间字符串解析本地时间
- 添加异常处理机制,当解析微信支付successTime失败时使用当前时间兜底
- 增加日志记录,便于排查时间解析异常问题
- 关闭MQTT服务启用开关以解决相关配置问题
2026-02-24 11:11:55 +08:00
be58a5cada fix(order): 修复订单支付时间设置逻辑
- 将支付时间设置为订单创建时间而非当前时间
- 添加订单状态检查逻辑
- 当订单状态为2时自动重置为0状态
- 确保支付成功后的时间戳一致性
2026-02-23 02:29:40 +08:00
eaea99a1e9 feat(controller): 添加硬删除功能并替换现有删除方法
- 在 BatchImportSupport 中新增 hardRemoveById 和 hardRemoveByIds 方法实现物理删除
- 替换所有控制器中的删除方法调用为新的硬删除方法
- 硬删除方法支持通过实体类和ID进行单个或批量物理删除
- 实现了分块处理大量ID的批量删除功能,避免数据库限制
- 保持 MyBatis-Plus 拦截器兼容性以
2026-02-14 18:28:47 +08:00
fbc3756ba7 feat(credit): 优化执行标的字段映射逻辑
- 修改CreditCourtSessionImportParam中occurrenceTime2字段的Excel名称为开庭时间
- 在CreditJudgmentDebtor实体中新增involvedAmountQcc字段用于存储执行标的金额
- 调整CreditJudgmentDebtorController中的执行标的选择逻辑,优先使用企查查字段
- 重新映射CreditJudicialDocumentImportParam中涉案金额字段的Excel注解配置
2026-02-14 17:56:53 +08:00
d177555ef9 feat(controller): 添加历史法院公告批量导入功能并优化Excel导入支持
- 新增批量导入历史法院公告接口,支持"历史法院公告"和"历史法庭公告"选项卡
- 实现数据库唯一索引约束防止重复数据导入
- 优化专利导入逻辑,优先读取"专利"选项卡并兼容多sheet格式
- 增强Excel导入头部匹配功能,支持括号标注的表头识别
- 添加全角半角括号统一处理和表头规范化映射
- 实现带括号后缀表头的智能匹配和剥离功能
- 新增专利导入相关单元测试验证括号表头处理
2026-02-14 16:52:02 +08:00
546027e7a4 fix(credit): 修正供应商实体字段描述并扩展搜索功能
- 将CreditSupplier实体中的purchaseAmount字段描述从"销售金额"更正为"采购金额"
- 更新CreditSupplierImportParam参数类中相应字段的Excel注解描述
- 在CreditSupplierMapper.xml中扩展关键词搜索功能,增加对supplier字段的搜索支持
2026-02-14 11:29:05 +08:00
82fe1ac24b fix(credit): 修正供应商实体中的字段描述
- 将CreditSupplier实体中的purchaseAmount字段描述从"采购金额(万元)"更正为"销售金额(万元)"
- 将CreditSupplierImportParam参数类中的purchaseAmount字段描述从"采购金额(万元)"更正为"销售金额(万元)"
2026-02-14 11:19:06 +08:00
2973844559 refactor(credit): 重构信用司法文书相关实体和参数类
- 调整CreditCourtSessionImportParam中字段顺序并优化Excel注解配置
- 将CreditJudicialDocument中的type字段重命名为documentType以提高语义清晰度
- 修改CreditJudicialDocument中involvedAmount字段描述去除单位后缀
- 更新CreditJudicialDocumentController中对documentType字段的映射逻辑
- 调整CreditJudicialDocumentImportParam中对应字段名称保持一致性
2026-02-14 11:10:46 +08:00
c5a942b4fc feat(credit): 优化信用消限记录导入功能支持多模板兼容
- 新增 application-glt.yml 生产环境配置文件
- 重构 CreditXgxf 实体类字段顺序和命名规范
- 添加上游多公司导出模板的备用字段映射支持
- 实现 Excel 表头括号后缀清理和标准化逻辑
- 增加备用字段(申请执行人、被执行人)的兼容性处理
- 完善导入参数转换逻辑确保模板兼容性
- 添加单元测试验证多模板字段映射正确性
2026-02-14 09:55:43 +08:00
bd3202830c feat(import): 实现批量导入去重机制并简化导入逻辑
- 在 BatchImportSupport 中新增 persistInsertOnlyChunk 方法处理仅插入模式的批量保存
- 新增 isDuplicateKey 方法用于检测数据库唯一索引冲突
- 修改行政许可、破产重整、失信被执行人等控制器的导入逻辑,使用新方法替换原有的 upsert 逻辑
- 移除 LinkedHashMap 的去重预处理,改为直接使用数据库唯一索引约束处理重复数据
- 更新导入规则描述,明确使用数据库唯一索引而非覆盖更新逻辑
- 移除 LocalDate 和 LinkedHashMap 等不再使用的导入包
2026-02-11 18:52:06 +08:00
0610f2c894 Merge remote-tracking branch 'origin/master' 2026-02-11 18:02:38 +08:00
95109bc031 refactor(credit): 重构客户导入功能的批处理逻辑
- 导入java.sql.SQLException依赖用于数据库异常处理
- 在处理导入参数时对客户名称进行预修剪操作
- 将原有的复杂批处理逻辑提取到persistImportChunk方法中
- 简化了批量导入的核心处理流程,提高代码可读性
- 优化了重复键检测逻辑,支持多种数据库错误码识别
- 移除了冗长的内联批处理实现,改用统一的方法调用
2026-02-11 18:02:31 +08:00
4fbd55cd41 feat(order): 完善送水订单与商城订单同步功能
- 在订单更新时增加租户ID获取逻辑并传递给相关服务方法
- 新增后台直接修改订单为已完成状态时同步商城订单的功能
- 优化数据库查询,使用COALESCE函数处理订单号显示逻辑
- 新增通过订单商品ID反向查找商城订单的兼容性处理
- 增加历史数据兜底机制,自动回填缺失的订单关联信息
- 添加详细的日志记录用于调试和监控订单同步状态
2026-02-10 17:27:45 +08:00
e1ef21f140 feat(order): 实现付款减库存功能并优化订单支付状态同步
- 在GltTicketOrder实体中新增orderNo字段用于订单编号关联
- 在订单查询SQL中关联glt_user_ticket表获取订单编号
- 新增DEDUCT_STOCK_TYPE_ORDER和DEDUCT_STOCK_TYPE_PAY常量定义库存扣除时机
- 实现下单时跳过付款减库存商品的库存扣除逻辑
- 实现订单取消时跳过付款减库存商品的库存回退逻辑
- 在ShopGoods实体中添加deductStockType字段支持库存计算方式配置
- 通过@JsonAlias注解支持前端字段别名映射
- 实现支付成功后触发付款减库存的库存扣除逻辑
- 添加deductStockAfterPaidIfNeeded方法处理支付后库存扣除
- 优化syncPaymentStatus方法确保支付状态同步时触发相关业务逻辑
- 添加重复支付状态检查避免重复执行支付成功业务逻辑
- 实现租户隔离的库存扣除操作和异常兜底机制
2026-02-10 17:05:57 +08:00
1177730464 perf(task): 调整定时任务执行频率以优化性能
- 将经销商佣金解冻任务执行频率从每分钟调整为每30秒一次
- 将经销商订单结算任务执行频率从每20秒调整为每10秒一次
- 将GLT套票发放任务执行频率从每分钟调整为每15秒一次
- 将GLT票券订单自动确认任务执行频率从每分钟调整为每33秒一次
- 将GLT用户票券自动释放任务执行频率从每分钟调整为每10分钟一次
- 在票券订单完成时同步更新商城订单状态为已完成
2026-02-10 13:52:13 +08:00
0fc914f47a fix(order): 解决送水订单完成后同步商城订单状态及关联记录数回填问题
- 在送水订单完成、确认收货和超时自动确认收货时同步更新关联商城订单状态为已完成
- 添加updateShopOrderOrderStatusAfterTicketFinished方法处理商城订单状态同步逻辑
- 在CreditCompanyRecordCountService中优化SQL查询方式,避免相关子查询导致的性能问题
- 将批量更新的chunk大小从1000调整为500以提高稳定性
- 在导入功能中添加异常处理,确保即使回填失败也不会影响整体导入流程
- 当关联记录数回填失败时在响应消息中添加警告信息
2026-02-10 12:52:27 +08:00
dd023bd2ca feat(order): 更新订单配送状态同步逻辑以支持配送员ID传递
- 在markShopOrderShippedAfterRiderAssigned方法中添加riderId参数
- 修改updateShopOrderDeliveryStatusAfterAccept方法以接收并处理配送员ID
- 更新查询逻辑以包含配送员ID字段的选择
- 添加实际配送员ID的判断逻辑,优先使用传入的配送员ID
- 修改配送状态更新条件,包含配送员ID不一致的场景
- 在更新配送状态的同时设置配送员ID
- 添加配送员ID查询条件到最终验证查询中
- 更新警告日志信息以包含配送员ID相关信息
2026-02-10 12:13:00 +08:00
ad5a5abb31 feat(order): 添加后台指派配送员时同步商城订单发货状态功能
- 在订单更新接口中添加配送员指派后的状态同步逻辑
- 新增 markShopOrderShippedAfterRiderAssigned 方法用于状态同步
- 实现后台指派配送员时自动将关联商城订单标记为已发货状态
- 添加相关业务方法注释说明使用场景和目的
- 确保配送员指派后订单状态的一致性同步
2026-02-10 12:08:58 +08:00
4481850809 feat(ticket): 接单时同步商城订单发货状态
- 在接单方法上添加事务注解确保操作原子性
- 优化时间获取逻辑避免重复调用当前时间
- 新增updateShopOrderDeliveryStatusAfterAccept方法处理发货状态同步
- 查询关联水票订单并更新对应商城订单的配送状态为20(已发货)
- 添加幂等性检查避免重复更新并记录异常情况日志
- 引入ShopOrder和ShopOrderService依赖支持订单状态更新
2026-02-10 11:29:00 +08:00
011c9e458a fix(order): 修复订单状态更新逻辑
- 移除订单状态字段的错误更新设置
- 移除发货状态字段的错误更新设置
- 保留礼品领取状态和更新时间的正确更新逻辑
2026-02-10 11:20:46 +08:00
4c8e67fe64 feat(task): 完善经销商佣金解冻任务功能
- 新增 ShopDealerOrderService 服务注入用于订单状态更新
- 添加 isUnfreeze 和 unfreezeTime 字段到 ShopDealerOrder 实体
- 实现佣金解冻完成后自动更新分销订单状态为"已解冻"
- 添加解冻时间记录功能
- 通过统计 flowType 判断佣金解冻完成度避免提前更新状态
- 增加解冻状态更新失败的日志警告
2026-02-10 10:46:05 +08:00
32db399cb5 refactor(shop): 重构ShopUser实体类和相关组件
- 调整ShopUser实体类字段定义,将id改为userId作为主键
- 更新用户类型枚举描述,从"个人用户/企业用户/其他"改为"普通用户/企业用户/特殊用户"
- 在birthday、settlementTime、createTime和updateTime字段添加@JsonFormat注解格式化输出
- 将offline字段类型从Boolean改为Integer以支持更多状态值
- 新增isDefault字段用于标识默认账号,解决多租户相同手机号问题
- 更新ShopUserController中的get方法参数从userId改为id并添加权限验证
- 注释掉ShopUserController中设置当前登录用户id的逻辑
- 修正ShopUserMapper.xml中查询条件的字段映射
- 在ShopUserParam中同步更新字段定义和查询条件配置
- 统一更新所有文件的since注释时间戳信息
2026-02-10 00:44:08 +08:00
d778036daf feat(ticket): 实现起始送水自动核销并生成送水订单功能
- 在自动核销成功后创建送水订单用于配送端跟踪
- 添加对用户水票更新失败的异常处理机制
- 添加对送水订单创建失败的异常处理机制
- 更新水票日志关联到送水订单并保留来源商城订单号
- 实现地址快照功能,优先使用地址表数据并兼容旧数据格式
- 添加安全字符串处理方法防止空指针异常
- 集成 ShopUserAddressService 和 GltTicketOrderService 服务
2026-02-09 20:39:27 +08:00
c1efeef8c7 refactor(ticket): 更新用户票务实体字段映射
- 将用户票务中的购买数量字段从buyQty替换为orderGoodsQty
- 移除GltUserTicket实体中的buyQty字段定义
- 添加orderGoodsQty字段到GltUserTicket实体中
- 修改服务层代码以使用订单商品总数量进行赋值
- 调整数据库映射关系以匹配新的业务逻辑
2026-02-09 20:28:32 +08:00
76aec53bae Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java
2026-02-09 18:56:33 +08:00
33f9f07037 feat(ticket): 添加购买数量字段到用户票务实体
- 在GltUserTicket实体中新增buyQty字段用于存储购买数量
- 在票务发放服务中设置购买数量到用户票务记录
- 更新实体注解以包含购买数量的描述信息
2026-02-09 18:55:17 +08:00
192 changed files with 9918 additions and 3542 deletions

View File

@@ -283,4 +283,6 @@ docker run -d -p 9200:9200 websoft-api
---
⭐ 如果这个项目对您有帮助,请给我们一个星标!
⭐ 如果这个项目对您有帮助,请给我们一个星标!

View File

@@ -0,0 +1,86 @@
package com.wechat.pay.java.core.exception;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.wechat.pay.java.core.http.HttpRequest;
import com.wechat.pay.java.core.util.GsonUtil;
/** 发送HTTP请求成功返回异常时抛出。例如返回状态码小于200或大于等于300、返回体参数不完整。 */
public class ServiceException extends WechatPayException {
private static final long serialVersionUID = -7174975090366956652L;
private final HttpRequest httpRequest;
private final int httpStatusCode;
private final String responseBody;
private String errorCode;
private String errorMessage;
/**
* 返回状态码小于200或大于300调用
*
* @param httpRequest http请求
* @param httpStatusCode http状态码
* @param responseBody http返回体
*/
public ServiceException(HttpRequest httpRequest, int httpStatusCode, String responseBody) {
super(
String.format(
"Wrong HttpStatusCode[%d]%nhttpResponseBody[%.1024s]\tHttpRequest[%s]",
httpStatusCode, responseBody, httpRequest));
this.httpRequest = httpRequest;
this.httpStatusCode = httpStatusCode;
this.responseBody = responseBody;
if (responseBody != null && !responseBody.isEmpty()) {
JsonObject jsonObject = GsonUtil.getGson().fromJson(responseBody, JsonObject.class);
JsonElement code = jsonObject.get("code");
JsonElement message = jsonObject.get("message");
this.errorCode = code == null ? null : code.getAsString();
this.errorMessage = message == null ? null : message.getAsString();
}
}
/**
* 获取序列化版本UID
*
* @return UID
*/
public static long getSerialVersionUID() {
return serialVersionUID;
}
/**
* 获取HTTP请求
*
* @return HTTP请求
*/
public HttpRequest getHttpRequest() {
return httpRequest;
}
/**
* 获取HTTP返回体
*
* @return HTTP返回体
*/
public String getResponseBody() {
return responseBody;
}
/**
* 获取HTTP状态码
*
* @return HTTP状态码
*/
public int getHttpStatusCode() {
return httpStatusCode;
}
public String getErrorCode() {
return errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
}

View File

@@ -0,0 +1,151 @@
package com.wechat.pay.java.core.http;
import static com.wechat.pay.java.core.http.Constant.ACCEPT;
import static com.wechat.pay.java.core.http.Constant.AUTHORIZATION;
import static com.wechat.pay.java.core.http.Constant.OS;
import static com.wechat.pay.java.core.http.Constant.REQUEST_ID;
import static com.wechat.pay.java.core.http.Constant.USER_AGENT;
import static com.wechat.pay.java.core.http.Constant.USER_AGENT_FORMAT;
import static com.wechat.pay.java.core.http.Constant.VERSION;
import static com.wechat.pay.java.core.http.Constant.WECHAT_PAY_SERIAL;
import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.util.Objects.requireNonNull;
import com.wechat.pay.java.core.auth.Credential;
import com.wechat.pay.java.core.auth.Validator;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.http.HttpRequest.Builder;
import java.io.InputStream;
/** 请求客户端抽象基类 */
public abstract class AbstractHttpClient implements HttpClient {
protected final Credential credential;
protected final Validator validator;
public AbstractHttpClient(Credential credential, Validator validator) {
this.credential = requireNonNull(credential);
this.validator = requireNonNull(validator);
}
@Override
public <T> HttpResponse<T> execute(HttpRequest httpRequest, Class<T> responseClass) {
HttpRequest innerRequest =
new Builder()
.url(httpRequest.getUrl())
.httpMethod(httpRequest.getHttpMethod())
.headers(httpRequest.getHeaders())
.addHeader(AUTHORIZATION, getAuthorization(httpRequest))
.addHeader(USER_AGENT, getUserAgent())
.addHeader(WECHAT_PAY_SERIAL, getWechatPaySerial())
.body(httpRequest.getBody())
.build();
OriginalResponse originalResponse = innerExecute(innerRequest);
validateResponse(originalResponse);
return assembleHttpResponse(originalResponse, responseClass);
}
@Override
public InputStream download(String url) {
HttpRequest originRequest =
new HttpRequest.Builder().httpMethod(HttpMethod.GET).url(url).build();
HttpRequest httpRequest =
new HttpRequest.Builder()
.url(url)
.httpMethod(HttpMethod.GET)
.addHeader(AUTHORIZATION, getAuthorization(originRequest))
.addHeader(ACCEPT, "*/*")
.addHeader(USER_AGENT, getUserAgent())
.addHeader(WECHAT_PAY_SERIAL, getWechatPaySerial())
.build();
return innerDownload(httpRequest);
}
protected abstract InputStream innerDownload(HttpRequest httpRequest);
protected abstract OriginalResponse innerExecute(HttpRequest httpRequest);
private void validateResponse(OriginalResponse originalResponse) {
if (isInvalidHttpCode(originalResponse.getStatusCode())) {
throw new ServiceException(
originalResponse.getRequest(),
originalResponse.getStatusCode(),
originalResponse.getBody());
}
if (originalResponse.getBody() != null
&& !originalResponse.getBody().isEmpty()
&& !MediaType.APPLICATION_JSON.equalsWith(originalResponse.getContentType())) {
throw new MalformedMessageException(
String.format(
"Unsupported content-type[%s]%nhttpRequest[%s]",
originalResponse.getContentType(), originalResponse.getRequest()));
}
if (!validator.validate(originalResponse.getHeaders(), originalResponse.getBody())) {
String requestId = originalResponse.getHeaders().getHeader(REQUEST_ID);
throw new ValidationException(
String.format(
"Validate response failed,the WechatPay signature is incorrect.%n"
+ "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
requestId, originalResponse.getHeaders(), originalResponse.getBody()));
}
}
protected boolean isInvalidHttpCode(int httpCode) {
return httpCode < HTTP_OK || httpCode >= HTTP_MULT_CHOICE;
}
private <T> HttpResponse<T> assembleHttpResponse(
OriginalResponse originalResponse, Class<T> responseClass) {
return new HttpResponse.Builder<T>()
.originalResponse(originalResponse)
.serviceResponseType(responseClass)
.build();
}
private String getSignBody(RequestBody requestBody) {
if (requestBody == null) {
return "";
}
if (requestBody instanceof JsonRequestBody) {
return ((JsonRequestBody) requestBody).getBody();
}
if (requestBody instanceof FileRequestBody) {
return ((FileRequestBody) requestBody).getMeta();
}
throw new UnsupportedOperationException(
String.format("Unsupported RequestBody Type[%s]", requestBody.getClass().getName()));
}
private String getUserAgent() {
return String.format(
USER_AGENT_FORMAT,
getClass().getPackage().getImplementationVersion(),
OS,
VERSION == null ? "Unknown" : VERSION,
credential.getClass().getSimpleName(),
validator.getClass().getSimpleName(),
getHttpClientInfo());
}
private String getWechatPaySerial() {
return this.validator.getSerialNumber();
}
/**
* 获取http客户端信息用于User-Agent。 格式:客户端名称/版本 示例okhttp3/4.9.3
*
* @return 客户端信息
*/
protected abstract String getHttpClientInfo();
private String getAuthorization(HttpRequest request) {
return credential.getAuthorization(
request.getUri(), request.getHttpMethod().name(), getSignBody(request.getBody()));
}
}

61
docs/ai/README.md Normal file
View File

@@ -0,0 +1,61 @@
# AI 模块Ollama + RAG + 订单分析)
## 1. 配置
`src/main/resources/application.yml`
- `ai.ollama.base-url`:主地址(例如 `https://ai-api.websoft.top`
- `ai.ollama.fallback-url`:备用地址(例如 `http://47.119.165.234:11434`
- `ai.ollama.chat-model`:对话模型(`qwen3.5:cloud`
- `ai.ollama.embed-model`:向量模型(`qwen3-embedding:4b`
## 2. 建表(知识库)
执行:`docs/ai/ai_kb_tables.sql`
## 3. API
说明:所有接口默认需要登录(`@PreAuthorize("isAuthenticated()")`),并且要求能够拿到 `tenantId`header 或登录用户)。
### 3.1 对话
- `GET /api/ai/models`:获取 Ollama 模型列表
- `POST /api/ai/chat`:非流式对话
- `POST /api/ai/chat/stream`流式对话SSE
- `GET /api/ai/chat/stream?prompt=...`流式对话SSE适配 EventSource
请求示例(非流式):
```json
{
"prompt": "帮我写一个退款流程说明"
}
```
### 3.2 知识库RAG
- `POST /api/ai/kb/upload`:上传文档入库(建议 txt/md/html
- `POST /api/ai/kb/sync/cms`:同步 CMS 已发布文章到知识库(当前租户)
- `POST /api/ai/kb/query`:仅检索 topK
- `POST /api/ai/kb/ask`:检索 + 生成答案(答案要求引用 chunk_id
请求示例ask
```json
{
"question": "怎么开具发票?",
"topK": 5
}
```
### 3.3 商城订单分析(按租户/按天)
- `POST /api/ai/analytics/query`:返回按天指标数据
- `POST /api/ai/analytics/ask`:基于指标数据生成分析结论
请求示例ask
```json
{
"question": "最近30天支付率有没有明显下滑请给出原因排查建议。",
"startDate": "2026-02-01",
"endDate": "2026-02-27"
}
```

39
docs/ai/ai_kb_tables.sql Normal file
View File

@@ -0,0 +1,39 @@
-- AI 知识库RAG建表脚本MySQL
-- 说明:本项目使用 MyBatis-Plus 默认命名规则AiKbDocument -> ai_kb_document
CREATE TABLE IF NOT EXISTS `ai_kb_document` (
`document_id` INT NOT NULL AUTO_INCREMENT COMMENT '文档ID',
`title` VARCHAR(255) NULL COMMENT '标题',
`source_type` VARCHAR(32) NULL COMMENT '来源类型upload/cms',
`source_id` INT NULL COMMENT '来源ID如 cms.article_id',
`source_ref` VARCHAR(255) NULL COMMENT '来源引用(如文件名、文章 code',
`content_hash` CHAR(64) NULL COMMENT '内容hash(SHA-256),用于增量同步',
`status` TINYINT NULL DEFAULT 0 COMMENT '状态',
`deleted` TINYINT NULL DEFAULT 0 COMMENT '逻辑删除0否1是',
`tenant_id` INT NOT NULL COMMENT '租户ID',
`update_time` DATETIME NULL COMMENT '更新时间',
`create_time` DATETIME NULL COMMENT '创建时间',
PRIMARY KEY (`document_id`),
KEY `idx_ai_kb_document_tenant` (`tenant_id`),
KEY `idx_ai_kb_document_source` (`tenant_id`, `source_type`, `source_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 知识库文档';
CREATE TABLE IF NOT EXISTS `ai_kb_chunk` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`document_id` INT NOT NULL COMMENT '文档ID',
`chunk_id` VARCHAR(64) NOT NULL COMMENT 'chunk 唯一ID用于引用',
`chunk_index` INT NULL COMMENT 'chunk 序号',
`title` VARCHAR(255) NULL COMMENT '标题(冗余,便于展示)',
`content` LONGTEXT NULL COMMENT 'chunk 文本',
`content_hash` CHAR(64) NULL COMMENT 'chunk 内容hash',
`embedding` LONGTEXT NULL COMMENT 'embedding(JSON数组)',
`embedding_norm` DOUBLE NULL COMMENT 'embedding L2 范数',
`deleted` TINYINT NULL DEFAULT 0 COMMENT '逻辑删除0否1是',
`tenant_id` INT NOT NULL COMMENT '租户ID',
`create_time` DATETIME NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ai_kb_chunk_chunk_id` (`chunk_id`),
KEY `idx_ai_kb_chunk_tenant` (`tenant_id`),
KEY `idx_ai_kb_chunk_document` (`document_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 知识库 chunk';

View File

@@ -0,0 +1,458 @@
# 客户跟进7步骤后端实现指南
## 📋 概述
本指南详细说明如何实现客户跟进7个步骤功能的后端代码包括数据库设计、Java后端实现和API接口。
## 🗄️ 数据库设计
### 1. 修改 credit_mp_customer 表结构
```sql
-- 为第5-7步添加字段第1-4步字段已存在
-- 第5步合同签订
ALTER TABLE credit_mp_customer ADD COLUMN follow_step5_submitted TINYINT DEFAULT 0 COMMENT '是否已提交';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step5_submitted_at VARCHAR(255) COMMENT '提交时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step5_contracts TEXT COMMENT '合同信息JSON数组';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step5_need_approval TINYINT DEFAULT 1 COMMENT '是否需要审核';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step5_approved TINYINT DEFAULT 0 COMMENT '是否审核通过';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step5_approved_at VARCHAR(255) COMMENT '审核时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step5_approved_by BIGINT COMMENT '审核人ID';
-- 第6步订单回款
ALTER TABLE credit_mp_customer ADD COLUMN follow_step6_submitted TINYINT DEFAULT 0 COMMENT '是否已提交';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step6_submitted_at VARCHAR(255) COMMENT '提交时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step6_payment_records TEXT COMMENT '财务录入的回款记录JSON数组';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step6_expected_payments TEXT COMMENT '预计回款JSON数组';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step6_need_approval TINYINT DEFAULT 1 COMMENT '是否需要审核';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step6_approved TINYINT DEFAULT 0 COMMENT '是否审核通过';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step6_approved_at VARCHAR(255) COMMENT '审核时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step6_approved_by BIGINT COMMENT '审核人ID';
-- 第7步电话回访
ALTER TABLE credit_mp_customer ADD COLUMN follow_step7_submitted TINYINT DEFAULT 0 COMMENT '是否已提交';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step7_submitted_at VARCHAR(255) COMMENT '提交时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step7_visit_records TEXT COMMENT '回访记录JSON数组';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step7_need_approval TINYINT DEFAULT 1 COMMENT '是否需要审核';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step7_approved TINYINT DEFAULT 0 COMMENT '是否审核通过';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step7_approved_at VARCHAR(255) COMMENT '审核时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step7_approved_by BIGINT COMMENT '审核人ID';
-- 添加流程结束相关字段
ALTER TABLE credit_mp_customer ADD COLUMN follow_process_ended TINYINT DEFAULT 0 COMMENT '流程是否已结束';
ALTER TABLE credit_mp_customer ADD COLUMN follow_process_end_time VARCHAR(255) COMMENT '流程结束时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_process_end_reason TEXT COMMENT '流程结束原因';
```
### 2. 创建审核记录表(可选)
```sql
CREATE TABLE credit_follow_approval (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
customer_id BIGINT NOT NULL COMMENT '客户ID',
step TINYINT NOT NULL COMMENT '步骤号',
approved TINYINT NOT NULL COMMENT '是否通过',
remark TEXT COMMENT '审核备注',
approved_by BIGINT COMMENT '审核人ID',
approved_at VARCHAR(255) COMMENT '审核时间',
created_at VARCHAR(255) COMMENT '创建时间',
INDEX idx_customer_step (customer_id, step),
INDEX idx_approved_by (approved_by)
) COMMENT='跟进步骤审核记录';
```
## ☕ Java后端实现
### 1. 实体类修改
```java
// CreditMpCustomer.java 添加字段
public class CreditMpCustomer {
// ... 现有字段
// 第5步字段
private Integer followStep5Submitted;
private String followStep5SubmittedAt;
private String followStep5Contracts;
private Integer followStep5NeedApproval;
private Integer followStep5Approved;
private String followStep5ApprovedAt;
private Long followStep5ApprovedBy;
// 第6步字段
private Integer followStep6Submitted;
private String followStep6SubmittedAt;
private String followStep6PaymentRecords;
private String followStep6ExpectedPayments;
private Integer followStep6NeedApproval;
private Integer followStep6Approved;
private String followStep6ApprovedAt;
private Long followStep6ApprovedBy;
// 第7步字段
private Integer followStep7Submitted;
private String followStep7SubmittedAt;
private String followStep7VisitRecords;
private Integer followStep7NeedApproval;
private Integer followStep7Approved;
private String followStep7ApprovedAt;
private Long followStep7ApprovedBy;
// 流程结束字段
private Integer followProcessEnded;
private String followProcessEndTime;
private String followProcessEndReason;
// getter/setter 方法...
}
```
### 2. DTO类创建
```java
// FollowStepApprovalDTO.java
@Data
public class FollowStepApprovalDTO {
private Long customerId;
private Integer step;
private Boolean approved;
private String remark;
}
// BatchFollowStepApprovalDTO.java
@Data
public class BatchFollowStepApprovalDTO {
private List<FollowStepApprovalDTO> approvals;
}
// FollowStatisticsDTO.java
@Data
public class FollowStatisticsDTO {
private Integer totalSteps;
private Integer completedSteps;
private Integer currentStep;
private Double progress;
private List<FollowStepDetailDTO> stepDetails;
}
// FollowStepDetailDTO.java
@Data
public class FollowStepDetailDTO {
private Integer step;
private String title;
private String status; // pending, submitted, approved, rejected
private String submittedAt;
private String approvedAt;
}
```
### 3. Service层实现
```java
// CreditMpCustomerServiceImpl.java 添加方法
@Service
public class CreditMpCustomerServiceImpl implements CreditMpCustomerService {
/**
* 审核跟进步骤
*/
@Override
@Transactional
public void approveFollowStep(FollowStepApprovalDTO dto) {
CreditMpCustomer customer = getById(dto.getCustomerId());
if (customer == null) {
throw new ServiceException("客户不存在");
}
// 验证步骤是否已提交
if (!isStepSubmitted(customer, dto.getStep())) {
throw new ServiceException("该步骤尚未提交,无法审核");
}
// 更新审核状态
updateStepApproval(customer, dto);
// 记录审核日志
saveApprovalLog(dto);
// 如果审核通过,更新客户步骤状态
if (dto.getApproved()) {
updateCustomerStep(customer, dto.getStep());
}
}
/**
* 批量审核跟进步骤
*/
@Override
@Transactional
public void batchApproveFollowSteps(BatchFollowStepApprovalDTO dto) {
for (FollowStepApprovalDTO approval : dto.getApprovals()) {
approveFollowStep(approval);
}
}
/**
* 获取待审核的跟进步骤列表
*/
@Override
public List<PendingApprovalStepVO> getPendingApprovalSteps(FollowStepQueryDTO query) {
return baseMapper.selectPendingApprovalSteps(query);
}
/**
* 获取客户跟进统计
*/
@Override
public FollowStatisticsDTO getFollowStatistics(Long customerId) {
CreditMpCustomer customer = getById(customerId);
if (customer == null) {
throw new ServiceException("客户不存在");
}
FollowStatisticsDTO statistics = new FollowStatisticsDTO();
statistics.setTotalSteps(7);
List<FollowStepDetailDTO> stepDetails = new ArrayList<>();
int completedSteps = 0;
int currentStep = 1;
for (int i = 1; i <= 7; i++) {
FollowStepDetailDTO detail = getStepDetail(customer, i);
stepDetails.add(detail);
if ("approved".equals(detail.getStatus())) {
completedSteps++;
currentStep = i + 1;
} else if ("submitted".equals(detail.getStatus()) && currentStep == 1) {
currentStep = i;
}
}
statistics.setCompletedSteps(completedSteps);
statistics.setCurrentStep(Math.min(currentStep, 7));
statistics.setProgress((double) completedSteps / 7 * 100);
statistics.setStepDetails(stepDetails);
return statistics;
}
/**
* 结束客户跟进流程
*/
@Override
@Transactional
public void endFollowProcess(Long customerId, String reason) {
CreditMpCustomer customer = getById(customerId);
if (customer == null) {
throw new ServiceException("客户不存在");
}
customer.setFollowProcessEnded(1);
customer.setFollowProcessEndTime(DateUtil.formatDateTime(new Date()));
customer.setFollowProcessEndReason(reason);
updateById(customer);
}
// 私有辅助方法...
private boolean isStepSubmitted(CreditMpCustomer customer, Integer step) {
switch (step) {
case 1: return customer.getFollowStep1Submitted() == 1;
case 2: return customer.getFollowStep2Submitted() == 1;
// ... 其他步骤
case 7: return customer.getFollowStep7Submitted() == 1;
default: return false;
}
}
private void updateStepApproval(CreditMpCustomer customer, FollowStepApprovalDTO dto) {
String currentTime = DateUtil.formatDateTime(new Date());
Long currentUserId = SecurityFrameworkUtils.getLoginUserId();
switch (dto.getStep()) {
case 5:
customer.setFollowStep5Approved(dto.getApproved() ? 1 : 0);
customer.setFollowStep5ApprovedAt(currentTime);
customer.setFollowStep5ApprovedBy(currentUserId);
break;
case 6:
customer.setFollowStep6Approved(dto.getApproved() ? 1 : 0);
customer.setFollowStep6ApprovedAt(currentTime);
customer.setFollowStep6ApprovedBy(currentUserId);
break;
case 7:
customer.setFollowStep7Approved(dto.getApproved() ? 1 : 0);
customer.setFollowStep7ApprovedAt(currentTime);
customer.setFollowStep7ApprovedBy(currentUserId);
break;
}
updateById(customer);
}
}
```
### 4. Controller层实现
```java
// CreditMpCustomerController.java 添加接口
@RestController
@RequestMapping("/credit/credit-mp-customer")
public class CreditMpCustomerController {
@PostMapping("/approve-follow-step")
@OperLog(title = "审核跟进步骤", businessType = BusinessType.UPDATE)
public R<Void> approveFollowStep(@RequestBody FollowStepApprovalDTO dto) {
creditMpCustomerService.approveFollowStep(dto);
return R.ok();
}
@PostMapping("/batch-approve-follow-steps")
@OperLog(title = "批量审核跟进步骤", businessType = BusinessType.UPDATE)
public R<Void> batchApproveFollowSteps(@RequestBody BatchFollowStepApprovalDTO dto) {
creditMpCustomerService.batchApproveFollowSteps(dto);
return R.ok();
}
@GetMapping("/pending-approval-steps")
@OperLog(title = "获取待审核跟进步骤", businessType = BusinessType.SELECT)
public R<List<PendingApprovalStepVO>> getPendingApprovalSteps(FollowStepQueryDTO query) {
List<PendingApprovalStepVO> list = creditMpCustomerService.getPendingApprovalSteps(query);
return R.ok(list);
}
@GetMapping("/follow-statistics/{customerId}")
@OperLog(title = "获取客户跟进统计", businessType = BusinessType.SELECT)
public R<FollowStatisticsDTO> getFollowStatistics(@PathVariable Long customerId) {
FollowStatisticsDTO statistics = creditMpCustomerService.getFollowStatistics(customerId);
return R.ok(statistics);
}
@PostMapping("/end-follow-process")
@OperLog(title = "结束客户跟进流程", businessType = BusinessType.UPDATE)
public R<Void> endFollowProcess(@RequestBody EndFollowProcessDTO dto) {
creditMpCustomerService.endFollowProcess(dto.getCustomerId(), dto.getReason());
return R.ok();
}
}
```
### 5. Mapper层SQL
```xml
<!-- CreditMpCustomerMapper.xml 添加查询方法 -->
<select id="selectPendingApprovalSteps" resultType="com.your.package.PendingApprovalStepVO">
SELECT
c.id as customerId,
c.to_user as customerName,
5 as step,
'合同签订' as stepTitle,
c.follow_step5_submitted_at as submittedAt,
u.real_name as submittedBy,
c.follow_step5_contracts as content
FROM credit_mp_customer c
LEFT JOIN sys_user u ON c.user_id = u.user_id
WHERE c.follow_step5_submitted = 1
AND c.follow_step5_approved = 0
AND c.deleted = 0
UNION ALL
SELECT
c.id as customerId,
c.to_user as customerName,
6 as step,
'订单回款' as stepTitle,
c.follow_step6_submitted_at as submittedAt,
u.real_name as submittedBy,
c.follow_step6_expected_payments as content
FROM credit_mp_customer c
LEFT JOIN sys_user u ON c.user_id = u.user_id
WHERE c.follow_step6_submitted = 1
AND c.follow_step6_approved = 0
AND c.deleted = 0
UNION ALL
SELECT
c.id as customerId,
c.to_user as customerName,
7 as step,
'电话回访' as stepTitle,
c.follow_step7_submitted_at as submittedAt,
u.real_name as submittedBy,
c.follow_step7_visit_records as content
FROM credit_mp_customer c
LEFT JOIN sys_user u ON c.user_id = u.user_id
WHERE c.follow_step7_submitted = 1
AND c.follow_step7_approved = 0
AND c.deleted = 0
<if test="step != null">
HAVING step = #{step}
</if>
<if test="customerId != null">
HAVING customerId = #{customerId}
</if>
ORDER BY submittedAt DESC
</select>
```
## 🔧 业务逻辑说明
### 1. 步骤解锁机制
- 第一步始终可用
- 后续步骤需要前一步审核通过才能进行
- 前端通过 `canEnterStep` 逻辑控制
### 2. 审核流程
- 步骤提交后设置 `needApproval = 1`
- 管理员在后台审核
- 审核通过后设置 `approved = 1` 并更新时间
### 3. 数据格式
- 所有复杂数据使用JSON格式存储
- 文件上传返回URL存储在JSON数组中
- 时间统一使用 `YYYY-MM-DD HH:mm:ss` 格式
### 4. 权限控制
- 销售只能提交和查看自己的客户
- 管理员可以审核所有步骤
- 财务人员可以录入第6步回款数据
## 📱 前端集成
前端代码已经完成,包括:
- 7个步骤的完整页面
- 步骤状态显示和跳转逻辑
- 数据提交和验证
- 客户详情页面的汇总显示
## 🚀 部署步骤
1. 执行数据库迁移脚本
2. 部署Java后端代码
3. 更新前端API调用
4. 测试完整流程
5. 配置权限和审核流程
## 📝 注意事项
1. **数据备份**:执行数据库变更前请备份
2. **权限配置**:确保各角色权限正确配置
3. **文件上传**:确认文件上传服务正常
4. **审核流程**:测试审核流程的完整性
5. **性能优化**:大量数据时考虑分页和索引优化
## 🔄 后续扩展
可以考虑的功能:
- 跟进模板和标准化流程
- 自动提醒和通知
- 数据统计和报表
- 跟进效率分析
- 客户满意度评估

View File

@@ -0,0 +1,34 @@
-- credit_user 索引优化MySQL/InnoDB
--
-- 背景:
-- - credit_user 列表分页默认排序sort_number asc, create_time desc
-- - 常见过滤tenant_idTenantLine 自动追加、deleted=0、company_id、user_id、create_time 范围
-- - 统计/刷新关联数WHERE deleted=0 AND company_id IN (...) GROUP BY company_id
--
-- 使用前建议先查看现有索引,避免重复:
-- SHOW INDEX FROM credit_user;
--
-- 注意:
-- - 大表加索引会消耗 IO/CPU建议在低峰执行。
-- - MySQL 8 可考虑ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONE视版本/引擎而定)。
-- 1) 覆盖默认分页排序 + 逻辑删除 + 租户隔离
-- 典型WHERE tenant_id=? AND deleted=0 ORDER BY sort_number, create_time DESC LIMIT ...
CREATE INDEX idx_credit_user_tenant_deleted_sort_create_id
ON credit_user (tenant_id, deleted, sort_number, create_time, id);
-- 2) 企业维度查询/统计(也服务于 credit_company 记录数刷新)
-- 典型WHERE tenant_id=? AND company_id=? AND deleted=0 ...
-- 典型WHERE tenant_id=? AND deleted=0 AND company_id IN (...) GROUP BY company_id
CREATE INDEX idx_credit_user_tenant_company_deleted
ON credit_user (tenant_id, company_id, deleted);
-- 3) 用户维度查询(后台常按录入人/负责人筛选)
-- 典型WHERE tenant_id=? AND user_id=? AND deleted=0 ...
CREATE INDEX idx_credit_user_tenant_user_deleted
ON credit_user (tenant_id, user_id, deleted);
-- 可选:如果你们经常按 type 过滤0/1再加这个否则先别加避免过多索引影响写入
-- CREATE INDEX idx_credit_user_tenant_type_deleted
-- ON credit_user (tenant_id, type, deleted);

View File

@@ -0,0 +1,194 @@
package com.gxwebsoft.ai.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gxwebsoft.ai.client.dto.*;
import com.gxwebsoft.ai.config.AiOllamaProperties;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.utils.JSONUtil;
import okhttp3.*;
import okio.BufferedSource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.time.Duration;
import java.util.Objects;
import java.util.function.Consumer;
/**
* 轻量 Ollama HTTP Client兼容 /api/chat、/api/embeddings、/api/tags
*/
@Component
public class OllamaClient {
@Resource
private AiOllamaProperties props;
@Resource
private ObjectMapper objectMapper;
private volatile OkHttpClient http;
private OkHttpClient http() {
OkHttpClient c = http;
if (c != null) {
return c;
}
synchronized (this) {
if (http == null) {
http = new OkHttpClient.Builder()
.connectTimeout(Duration.ofMillis(props.getConnectTimeoutMs()))
.readTimeout(Duration.ofMillis(props.getReadTimeoutMs()))
.writeTimeout(Duration.ofMillis(props.getWriteTimeoutMs()))
.build();
}
return http;
}
}
public OllamaTagsResponse tags() {
return getJson("/api/tags", OllamaTagsResponse.class);
}
public OllamaChatResponse chat(OllamaChatRequest req) {
if (req.getStream() == null) {
req.setStream(false);
}
return postJson("/api/chat", req, OllamaChatResponse.class);
}
/**
* 流式对话Ollama 会返回按行分隔的 JSON。
*/
public void chatStream(OllamaChatRequest req, Consumer<OllamaChatResponse> onEvent) {
Objects.requireNonNull(onEvent, "onEvent");
if (req.getStream() == null) {
req.setStream(true);
}
Request request = buildPost(baseUrl(), "/api/chat", JSONUtil.toJSONString(req));
try (Response resp = http().newCall(request).execute()) {
if (!resp.isSuccessful()) {
throw new BusinessException("Ollama chat stream failed: HTTP " + resp.code());
}
ResponseBody body = resp.body();
if (body == null) {
throw new BusinessException("Ollama chat stream failed: empty body");
}
BufferedSource source = body.source();
String line;
while ((line = source.readUtf8Line()) != null) {
line = line.trim();
if (line.isEmpty()) {
continue;
}
OllamaChatResponse event = objectMapper.readValue(line, OllamaChatResponse.class);
onEvent.accept(event);
if (Boolean.TRUE.equals(event.getDone())) {
break;
}
}
} catch (IOException e) {
throw new BusinessException("Ollama chat stream IO error: " + e.getMessage());
}
}
public OllamaEmbeddingResponse embedding(String prompt) {
OllamaEmbeddingRequest req = new OllamaEmbeddingRequest();
req.setModel(props.getEmbedModel());
req.setPrompt(prompt);
return postJson("/api/embeddings", req, OllamaEmbeddingResponse.class);
}
private String baseUrl() {
if (props.getBaseUrl() == null || props.getBaseUrl().trim().isEmpty()) {
throw new BusinessException("ai.ollama.base-url 未配置");
}
return props.getBaseUrl().trim();
}
private String fallbackUrl() {
if (props.getFallbackUrl() == null || props.getFallbackUrl().trim().isEmpty()) {
return null;
}
return props.getFallbackUrl().trim();
}
private <T> T getJson(String path, Class<T> clazz) {
try {
return getJsonOnce(baseUrl(), path, clazz);
} catch (Exception e) {
String fb = fallbackUrl();
if (fb == null) {
throw e;
}
return getJsonOnce(fb, path, clazz);
}
}
private <T> T getJsonOnce(String base, String path, Class<T> clazz) {
Request req = new Request.Builder()
.url(join(base, path))
.get()
.build();
try (Response resp = http().newCall(req).execute()) {
if (!resp.isSuccessful()) {
throw new BusinessException("Ollama GET failed: HTTP " + resp.code());
}
ResponseBody body = resp.body();
if (body == null) {
throw new BusinessException("Ollama GET failed: empty body");
}
return objectMapper.readValue(body.string(), clazz);
} catch (IOException e) {
throw new BusinessException("Ollama GET IO error: " + e.getMessage());
}
}
private <T> T postJson(String path, Object payload, Class<T> clazz) {
String json = JSONUtil.toJSONString(payload);
try {
return postJsonOnce(baseUrl(), path, json, clazz);
} catch (Exception e) {
String fb = fallbackUrl();
if (fb == null) {
throw e;
}
return postJsonOnce(fb, path, json, clazz);
}
}
private <T> T postJsonOnce(String base, String path, String json, Class<T> clazz) {
Request req = buildPost(base, path, json);
try (Response resp = http().newCall(req).execute()) {
if (!resp.isSuccessful()) {
throw new BusinessException("Ollama POST failed: HTTP " + resp.code());
}
ResponseBody body = resp.body();
if (body == null) {
throw new BusinessException("Ollama POST failed: empty body");
}
return objectMapper.readValue(body.string(), clazz);
} catch (IOException e) {
throw new BusinessException("Ollama POST IO error: " + e.getMessage());
}
}
private Request buildPost(String base, String path, String json) {
RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
return new Request.Builder()
.url(join(base, path))
.post(body)
.build();
}
private static String join(String base, String path) {
String b = base;
if (b.endsWith("/")) {
b = b.substring(0, b.length() - 1);
}
String p = path.startsWith("/") ? path : ("/" + path);
return b + p;
}
}

View File

@@ -0,0 +1,19 @@
package com.gxwebsoft.ai.client.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class OllamaChatRequest {
private String model;
private List<OllamaMessage> messages;
private Boolean stream;
/**
* Ollama options例如temperature、top_k、top_p、num_predict...
*/
private Map<String, Object> options;
}

View File

@@ -0,0 +1,28 @@
package com.gxwebsoft.ai.client.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OllamaChatResponse {
private String model;
@JsonProperty("created_at")
private String createdAt;
private OllamaMessage message;
private Boolean done;
@JsonProperty("total_duration")
private Long totalDuration;
@JsonProperty("prompt_eval_count")
private Integer promptEvalCount;
@JsonProperty("eval_count")
private Integer evalCount;
}

View File

@@ -0,0 +1,14 @@
package com.gxwebsoft.ai.client.dto;
import lombok.Data;
@Data
public class OllamaEmbeddingRequest {
private String model;
/**
* Ollama embeddings 目前常用字段为 prompt。
*/
private String prompt;
}

View File

@@ -0,0 +1,13 @@
package com.gxwebsoft.ai.client.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OllamaEmbeddingResponse {
private List<Double> embedding;
}

View File

@@ -0,0 +1,14 @@
package com.gxwebsoft.ai.client.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OllamaMessage {
private String role;
private String content;
}

View File

@@ -0,0 +1,22 @@
package com.gxwebsoft.ai.client.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OllamaTagsResponse {
private List<Model> models;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Model {
private String name;
private Long size;
private String digest;
private String modified_at;
}
}

View File

@@ -0,0 +1,68 @@
package com.gxwebsoft.ai.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Ollama API 配置。
*
* 说明:本项目通过自建 Ollama 网关提供服务,因此这里用 baseUrl + fallbackUrl。
*/
@Data
@Component
@ConfigurationProperties(prefix = "ai.ollama")
public class AiOllamaProperties {
/**
* 主地址例如https://ai-api.websoft.top
*/
private String baseUrl;
/**
* 备用地址例如http://47.119.165.234:11434
*/
private String fallbackUrl;
/**
* 对话模型例如qwen3.5:cloud
*/
private String chatModel;
/**
* 向量模型例如qwen3-embedding:4b
*/
private String embedModel;
/**
* HTTP 超时(毫秒)。
*/
private long connectTimeoutMs = 10_000;
private long readTimeoutMs = 300_000;
private long writeTimeoutMs = 60_000;
/**
* 并发上限(用于 embedding/入库等批处理场景)。
*/
private int maxConcurrency = 4;
/**
* RAG检索候选 chunk 最大数量(避免一次性拉取过多数据导致内存/耗时过高)。
*/
private int ragMaxCandidates = 2000;
/**
* RAG检索返回 topK。
*/
private int ragTopK = 5;
/**
* RAG单个 chunk 最大字符数(用于入库切分)。
*/
private int ragChunkSize = 800;
/**
* RAGchunk 重叠字符数(用于减少语义断裂)。
*/
private int ragChunkOverlap = 120;
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.ai.controller;
import com.gxwebsoft.ai.dto.*;
import com.gxwebsoft.ai.service.AiAnalyticsService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@Tag(name = "AI - 订单数据分析")
@RestController
@RequestMapping("/api/ai/analytics")
public class AiAnalyticsController extends BaseController {
@Resource
private AiAnalyticsService analyticsService;
@PreAuthorize("isAuthenticated()")
@Operation(summary = "查询商城订单按天指标(当前租户)")
@PostMapping("/query")
public ApiResult<AiShopMetricsQueryResult> query(@RequestBody AiShopMetricsQueryRequest request) {
Integer tenantId = getTenantId();
return success(analyticsService.queryShopMetrics(tenantId, request));
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "AI 解析并输出订单分析结论(当前租户)")
@PostMapping("/ask")
public ApiResult<AiAnalyticsAskResult> ask(@RequestBody AiAnalyticsAskRequest request) {
Integer tenantId = getTenantId();
return success(analyticsService.askShopAnalytics(tenantId, request));
}
}

View File

@@ -0,0 +1,95 @@
package com.gxwebsoft.ai.controller;
import com.gxwebsoft.ai.client.OllamaClient;
import com.gxwebsoft.ai.client.dto.OllamaChatResponse;
import com.gxwebsoft.ai.client.dto.OllamaTagsResponse;
import com.gxwebsoft.ai.dto.AiChatRequest;
import com.gxwebsoft.ai.dto.AiChatResult;
import com.gxwebsoft.ai.dto.AiMessage;
import com.gxwebsoft.ai.service.AiChatService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Tag(name = "AI - 对话")
@RestController
@RequestMapping("/api/ai")
public class AiChatController extends BaseController {
@Resource
private OllamaClient ollamaClient;
@Resource
private AiChatService aiChatService;
@PreAuthorize("isAuthenticated()")
@Operation(summary = "获取 Ollama 模型列表")
@GetMapping("/models")
public ApiResult<OllamaTagsResponse> models() {
return success(ollamaClient.tags());
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "非流式对话")
@PostMapping("/chat")
public ApiResult<AiChatResult> chat(@RequestBody AiChatRequest request) {
return success(aiChatService.chat(request));
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "流式对话SSE")
@PostMapping("/chat/stream")
public SseEmitter chatStream(@RequestBody AiChatRequest request) {
// 10 分钟超时(可根据前端需要调整)
SseEmitter emitter = new SseEmitter(10 * 60 * 1000L);
CompletableFuture.runAsync(() -> {
try {
aiChatService.chatStream(request,
delta -> {
try {
emitter.send(SseEmitter.event().name("delta").data(delta));
} catch (Exception e) {
// 客户端断开会触发 send 异常,这里直接结束即可
emitter.complete();
}
},
(OllamaChatResponse done) -> {
try {
Map<String, Object> meta = new LinkedHashMap<>();
meta.put("model", done.getModel());
meta.put("prompt_eval_count", done.getPromptEvalCount());
meta.put("eval_count", done.getEvalCount());
meta.put("total_duration", done.getTotalDuration());
emitter.send(SseEmitter.event().name("done").data(meta));
} catch (Exception ignored) {
} finally {
emitter.complete();
}
});
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "流式对话SSE, GET 版本,便于 EventSource")
@GetMapping("/chat/stream")
public SseEmitter chatStreamGet(@RequestParam("prompt") String prompt) {
AiChatRequest req = new AiChatRequest();
req.setMessages(Collections.singletonList(new AiMessage("user", prompt)));
return chatStream(req);
}
}

View File

@@ -0,0 +1,54 @@
package com.gxwebsoft.ai.controller;
import com.gxwebsoft.ai.dto.*;
import com.gxwebsoft.ai.service.AiKbRagService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
@Tag(name = "AI - 知识库(RAG)")
@RestController
@RequestMapping("/api/ai/kb")
public class AiKbController extends BaseController {
@Resource
private AiKbRagService ragService;
@PreAuthorize("isAuthenticated()")
@Operation(summary = "上传文档入库txt/md/html 优先)")
@PostMapping("/upload")
public ApiResult<AiKbIngestResult> upload(@RequestParam("file") MultipartFile file) {
Integer tenantId = getTenantId();
return success(ragService.ingestUpload(tenantId, file));
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "同步 CMS 文章到知识库(当前租户)")
@PostMapping("/sync/cms")
public ApiResult<AiKbIngestResult> syncCms() {
Integer tenantId = getTenantId();
return success(ragService.syncCms(tenantId));
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "仅检索(返回 topK 命中)")
@PostMapping("/query")
public ApiResult<AiKbQueryResult> query(@RequestBody AiKbQueryRequest request) {
Integer tenantId = getTenantId();
return success(ragService.query(tenantId, request));
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "知识库问答RAG 生成 + 返回检索结果)")
@PostMapping("/ask")
public ApiResult<AiKbAskResult> ask(@RequestBody AiKbAskRequest request) {
Integer tenantId = getTenantId();
return success(ragService.ask(tenantId, request));
}
}

View File

@@ -0,0 +1,13 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AiAnalyticsAskRequest", description = "AI 数据分析提问")
public class AiAnalyticsAskRequest {
private String question;
private String startDate;
private String endDate;
}

View File

@@ -0,0 +1,12 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AiAnalyticsAskResult", description = "AI 数据分析结果")
public class AiAnalyticsAskResult {
private String analysis;
private AiShopMetricsQueryResult data;
}

View File

@@ -0,0 +1,35 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(name = "AiChatRequest", description = "AI 对话请求")
public class AiChatRequest {
@Schema(description = "可选:直接传一句话。若 messages 为空则使用该字段构造 user message")
private String prompt;
@Schema(description = "可选OpenAI 风格 messagesrole: system/user/assistant")
private List<AiMessage> messages;
@Schema(description = "可选:覆盖默认模型")
private String model;
@Schema(description = "是否流式输出(/chat/stream 端点通常忽略此字段)")
private Boolean stream;
@Schema(description = "temperature")
private Double temperature;
@Schema(description = "top_k")
private Integer topK;
@Schema(description = "top_p")
private Double topP;
@Schema(description = "num_predict类似 max_tokens")
private Integer numPredict;
}

View File

@@ -0,0 +1,15 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "AiChatResult", description = "AI 对话结果")
public class AiChatResult {
private String content;
}

View File

@@ -0,0 +1,12 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AiKbAskRequest", description = "知识库问答请求")
public class AiKbAskRequest {
private String question;
private Integer topK;
}

View File

@@ -0,0 +1,12 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AiKbAskResult", description = "知识库问答结果")
public class AiKbAskResult {
private String answer;
private AiKbQueryResult retrieval;
}

View File

@@ -0,0 +1,15 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AiKbHit", description = "知识库命中")
public class AiKbHit {
private String chunkId;
private Integer documentId;
private String title;
private Double score;
private String content;
}

View File

@@ -0,0 +1,15 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AiKbIngestResult", description = "知识库入库结果")
public class AiKbIngestResult {
private Integer documentId;
private String title;
private Integer chunks;
private Integer updatedDocuments;
private Integer skippedDocuments;
}

View File

@@ -0,0 +1,12 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AiKbQueryRequest", description = "知识库检索请求")
public class AiKbQueryRequest {
private String query;
private Integer topK;
}

View File

@@ -0,0 +1,13 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(name = "AiKbQueryResult", description = "知识库检索结果")
public class AiKbQueryResult {
private List<AiKbHit> hits;
}

View File

@@ -0,0 +1,14 @@
package com.gxwebsoft.ai.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AiMessage {
private String role;
private String content;
}

View File

@@ -0,0 +1,18 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AiShopMetricsQueryRequest", description = "商城订单指标查询请求")
public class AiShopMetricsQueryRequest {
@Schema(description = "开始日期(YYYY-MM-DD)")
private String startDate;
@Schema(description = "结束日期(YYYY-MM-DD),包含该天")
private String endDate;
@Schema(description = "是否按天分组,默认 true")
private Boolean groupByDay;
}

View File

@@ -0,0 +1,16 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(name = "AiShopMetricsQueryResult", description = "商城订单指标查询结果")
public class AiShopMetricsQueryResult {
private Integer tenantId;
private String startDate;
private String endDate;
private List<AiShopMetricsRow> rows;
}

View File

@@ -0,0 +1,23 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(name = "AiShopMetricsRow", description = "商城订单指标行(按 tenant/day")
public class AiShopMetricsRow {
private Integer tenantId;
private String day;
private Long orderCnt;
private Long paidOrderCnt;
private BigDecimal gmv;
private BigDecimal refundAmt;
private Long payUserCnt;
private BigDecimal aov;
private BigDecimal payRate;
}

View File

@@ -0,0 +1,57 @@
package com.gxwebsoft.ai.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 知识库分段chunk
*/
@Data
@Schema(name = "AiKbChunk", description = "AI 知识库分段")
public class AiKbChunk implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private Integer documentId;
/**
* 外部引用用的唯一 ID便于在答案里引用
*/
private String chunkId;
private Integer chunkIndex;
private String title;
private String content;
private String contentHash;
/**
* embedding JSON数组存成文本便于快速落库。
*/
private String embedding;
/**
* embedding 的 L2 范数,用于余弦相似度。
*/
private Double embeddingNorm;
@TableLogic
private Integer deleted;
private Integer tenantId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,59 @@
package com.gxwebsoft.ai.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 知识库文档来源上传、CMS 等)。
*/
@Data
@Schema(name = "AiKbDocument", description = "AI 知识库文档")
public class AiKbDocument implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "document_id", type = IdType.AUTO)
private Integer documentId;
private String title;
/**
* upload / cms
*/
private String sourceType;
/**
* 例如 CMS article_id
*/
private Integer sourceId;
/**
* 例如文件名、路径等
*/
private String sourceRef;
/**
* 文档文本内容哈希(用于增量同步/去重)。
*/
private String contentHash;
private Integer status;
@TableLogic
private Integer deleted;
private Integer tenantId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,10 @@
package com.gxwebsoft.ai.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.ai.entity.AiKbChunk;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiKbChunkMapper extends BaseMapper<AiKbChunk> {
}

View File

@@ -0,0 +1,10 @@
package com.gxwebsoft.ai.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.ai.entity.AiKbDocument;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiKbDocumentMapper extends BaseMapper<AiKbDocument> {
}

View File

@@ -0,0 +1,16 @@
package com.gxwebsoft.ai.mapper;
import com.gxwebsoft.ai.dto.AiShopMetricsRow;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface AiShopAnalyticsMapper {
List<AiShopMetricsRow> queryMetrics(@Param("tenantId") Integer tenantId,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.ai.mapper.AiShopAnalyticsMapper">
<select id="queryMetrics" resultType="com.gxwebsoft.ai.dto.AiShopMetricsRow">
SELECT
tenant_id,
DATE_FORMAT(create_time, '%Y-%m-%d') AS day,
COUNT(1) AS order_cnt,
SUM(CASE WHEN pay_status = 1 THEN 1 ELSE 0 END) AS paid_order_cnt,
SUM(CASE WHEN pay_status = 1 THEN COALESCE(pay_price, 0) ELSE 0 END) AS gmv,
SUM(CASE WHEN COALESCE(refund_money, 0) &gt; 0 THEN COALESCE(refund_money, 0) ELSE 0 END) AS refund_amt,
COUNT(DISTINCT IF(pay_status = 1, user_id, NULL)) AS pay_user_cnt
FROM shop_order
WHERE deleted = 0
AND tenant_id = #{tenantId}
AND create_time &gt;= #{start}
AND create_time &lt; #{end}
GROUP BY tenant_id, DATE_FORMAT(create_time, '%Y-%m-%d')
ORDER BY day ASC
</select>
</mapper>

View File

@@ -0,0 +1,24 @@
package com.gxwebsoft.ai.prompt;
/**
* 统一提示词模板(尽量简短、可控)。
*/
public class AiPrompts {
private AiPrompts() {
}
public static final String SYSTEM_SUPPORT =
"你是 WebSoft 客服AI。规则\n" +
"- 只使用给定的“上下文资料”回答,禁止编造。\n" +
"- 如果资料不足,直接说“资料不足”,并列出需要补充的信息。\n" +
"- 答案末尾必须给引用,格式:[source:chunk_id]。\n" +
"- 输出中文,简洁可执行。\n";
public static final String SYSTEM_ANALYTICS =
"你是商城订单数据分析助手。你将基于提供的按天指标数据给出结论。\n" +
"要求:\n" +
"- 只基于数据陈述,不要编造不存在的数字。\n" +
"- 输出包含:结论、关键指标变化、异常点、建议的下一步核查。\n" +
"- 输出中文,简洁。\n";
}

View File

@@ -0,0 +1,82 @@
package com.gxwebsoft.ai.service;
import cn.hutool.core.util.StrUtil;
import com.gxwebsoft.ai.dto.*;
import com.gxwebsoft.ai.prompt.AiPrompts;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.utils.JSONUtil;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
@Service
public class AiAnalyticsService {
@Resource
private AiShopAnalyticsService shopAnalyticsService;
@Resource
private AiChatService aiChatService;
public AiShopMetricsQueryResult queryShopMetrics(Integer tenantId, AiShopMetricsQueryRequest request) {
if (tenantId == null) {
throw new BusinessException("tenantId 不能为空");
}
if (request == null) {
throw new BusinessException("请求不能为空");
}
LocalDate start = parseDate(request.getStartDate(), "startDate");
LocalDate end = parseDate(request.getEndDate(), "endDate");
if (end.isBefore(start)) {
throw new BusinessException("endDate 不能早于 startDate");
}
AiShopMetricsQueryResult r = new AiShopMetricsQueryResult();
r.setTenantId(tenantId);
r.setStartDate(start.toString());
r.setEndDate(end.toString());
r.setRows(shopAnalyticsService.queryTenantDaily(tenantId, start, end));
return r;
}
public AiAnalyticsAskResult askShopAnalytics(Integer tenantId, AiAnalyticsAskRequest request) {
if (request == null || StrUtil.isBlank(request.getQuestion())) {
throw new BusinessException("question 不能为空");
}
AiShopMetricsQueryRequest q = new AiShopMetricsQueryRequest();
q.setStartDate(request.getStartDate());
q.setEndDate(request.getEndDate());
q.setGroupByDay(true);
AiShopMetricsQueryResult data = queryShopMetrics(tenantId, q);
String userPrompt =
"用户问题:\n" + request.getQuestion() + "\n\n" +
"数据JSON字段含义order_cnt=订单数paid_order_cnt=已支付订单数gmv=已支付金额refund_amt=退款金额pay_user_cnt=支付用户数aov=客单价pay_rate=支付率):\n" +
JSONUtil.toJSONString(data, true);
AiChatRequest chat = new AiChatRequest();
chat.setMessages(Arrays.asList(
new AiMessage("system", AiPrompts.SYSTEM_ANALYTICS),
new AiMessage("user", userPrompt)
));
AiChatResult resp = aiChatService.chat(chat);
AiAnalyticsAskResult r = new AiAnalyticsAskResult();
r.setAnalysis(resp.getContent());
r.setData(data);
return r;
}
private static LocalDate parseDate(String s, String field) {
if (StrUtil.isBlank(s)) {
throw new BusinessException(field + " 不能为空,格式 YYYY-MM-DD");
}
try {
return LocalDate.parse(s.trim());
} catch (DateTimeParseException e) {
throw new BusinessException(field + " 格式错误,需 YYYY-MM-DD");
}
}
}

View File

@@ -0,0 +1,94 @@
package com.gxwebsoft.ai.service;
import cn.hutool.core.util.StrUtil;
import com.gxwebsoft.ai.client.OllamaClient;
import com.gxwebsoft.ai.client.dto.OllamaChatRequest;
import com.gxwebsoft.ai.client.dto.OllamaChatResponse;
import com.gxwebsoft.ai.client.dto.OllamaMessage;
import com.gxwebsoft.ai.config.AiOllamaProperties;
import com.gxwebsoft.ai.dto.AiChatRequest;
import com.gxwebsoft.ai.dto.AiChatResult;
import com.gxwebsoft.ai.dto.AiMessage;
import com.gxwebsoft.common.core.exception.BusinessException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.*;
import java.util.function.Consumer;
@Service
public class AiChatService {
@Resource
private AiOllamaProperties props;
@Resource
private OllamaClient ollamaClient;
public AiChatResult chat(AiChatRequest request) {
OllamaChatRequest req = buildChatRequest(request, false);
OllamaChatResponse resp = ollamaClient.chat(req);
String content = resp != null && resp.getMessage() != null ? resp.getMessage().getContent() : null;
return new AiChatResult(content == null ? "" : content);
}
public void chatStream(AiChatRequest request, Consumer<String> onDelta, Consumer<OllamaChatResponse> onFinal) {
Objects.requireNonNull(onDelta, "onDelta");
OllamaChatRequest req = buildChatRequest(request, true);
ollamaClient.chatStream(req, event -> {
String delta = event != null && event.getMessage() != null ? event.getMessage().getContent() : null;
if (StrUtil.isNotBlank(delta)) {
onDelta.accept(delta);
}
if (Boolean.TRUE.equals(event.getDone()) && onFinal != null) {
onFinal.accept(event);
}
});
}
private OllamaChatRequest buildChatRequest(AiChatRequest request, boolean stream) {
if (request == null) {
throw new BusinessException("请求不能为空");
}
List<AiMessage> messages = request.getMessages();
if ((messages == null || messages.isEmpty()) && StrUtil.isBlank(request.getPrompt())) {
throw new BusinessException("prompt 或 messages 不能为空");
}
if (messages == null || messages.isEmpty()) {
messages = Collections.singletonList(new AiMessage("user", request.getPrompt()));
}
List<OllamaMessage> ollamaMessages = new ArrayList<>();
for (AiMessage m : messages) {
if (m == null || StrUtil.isBlank(m.getRole()) || m.getContent() == null) {
continue;
}
ollamaMessages.add(new OllamaMessage(m.getRole(), m.getContent()));
}
if (ollamaMessages.isEmpty()) {
throw new BusinessException("messages 为空或无有效内容");
}
Map<String, Object> options = new HashMap<>();
if (request.getTemperature() != null) {
options.put("temperature", request.getTemperature());
}
if (request.getTopK() != null) {
options.put("top_k", request.getTopK());
}
if (request.getTopP() != null) {
options.put("top_p", request.getTopP());
}
if (request.getNumPredict() != null) {
options.put("num_predict", request.getNumPredict());
}
OllamaChatRequest req = new OllamaChatRequest();
req.setModel(StrUtil.blankToDefault(request.getModel(), props.getChatModel()));
req.setMessages(ollamaMessages);
req.setStream(stream);
req.setOptions(options.isEmpty() ? null : options);
return req;
}
}

View File

@@ -0,0 +1,8 @@
package com.gxwebsoft.ai.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.ai.entity.AiKbChunk;
public interface AiKbChunkService extends IService<AiKbChunk> {
}

View File

@@ -0,0 +1,8 @@
package com.gxwebsoft.ai.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.ai.entity.AiKbDocument;
public interface AiKbDocumentService extends IService<AiKbDocument> {
}

View File

@@ -0,0 +1,426 @@
package com.gxwebsoft.ai.service;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.ai.client.OllamaClient;
import com.gxwebsoft.ai.client.dto.OllamaEmbeddingResponse;
import com.gxwebsoft.ai.config.AiOllamaProperties;
import com.gxwebsoft.ai.dto.*;
import com.gxwebsoft.ai.entity.AiKbChunk;
import com.gxwebsoft.ai.entity.AiKbDocument;
import com.gxwebsoft.ai.prompt.AiPrompts;
import com.gxwebsoft.ai.util.AiTextUtil;
import com.gxwebsoft.cms.entity.CmsArticle;
import com.gxwebsoft.cms.entity.CmsArticleContent;
import com.gxwebsoft.cms.service.CmsArticleContentService;
import com.gxwebsoft.cms.service.CmsArticleService;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.utils.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.tika.Tika;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
@Service
public class AiKbRagService {
@Resource
private AiOllamaProperties props;
@Resource
private OllamaClient ollamaClient;
@Resource
private AiKbDocumentService documentService;
@Resource
private AiKbChunkService chunkService;
@Resource
private CmsArticleService cmsArticleService;
@Resource
private CmsArticleContentService cmsArticleContentService;
@Resource
private AiChatService aiChatService;
@Resource
private ObjectMapper objectMapper;
private final Tika tika = new Tika();
private volatile ExecutorService embedPool;
private ExecutorService pool() {
ExecutorService p = embedPool;
if (p != null) {
return p;
}
synchronized (this) {
if (embedPool == null) {
embedPool = Executors.newFixedThreadPool(Math.max(1, props.getMaxConcurrency()));
}
return embedPool;
}
}
public AiKbIngestResult ingestUpload(Integer tenantId, MultipartFile file) {
if (tenantId == null) {
throw new BusinessException("tenantId 不能为空");
}
if (file == null || file.isEmpty()) {
throw new BusinessException("文件不能为空");
}
String title = StrUtil.blankToDefault(file.getOriginalFilename(), "upload");
String text = extractText(file);
if (StrUtil.isBlank(text)) {
throw new BusinessException("无法解析文件内容,请上传 txt/md/html 等可解析文本");
}
String contentHash = AiTextUtil.sha256(text);
AiKbDocument doc = new AiKbDocument();
doc.setTitle(title);
doc.setSourceType("upload");
doc.setSourceRef(title);
doc.setContentHash(contentHash);
doc.setStatus(0);
doc.setTenantId(tenantId);
doc.setCreateTime(LocalDateTime.now());
doc.setUpdateTime(LocalDateTime.now());
documentService.save(doc);
int chunks = ingestChunks(doc, text);
AiKbIngestResult r = new AiKbIngestResult();
r.setDocumentId(doc.getDocumentId());
r.setTitle(doc.getTitle());
r.setChunks(chunks);
r.setUpdatedDocuments(1);
r.setSkippedDocuments(0);
return r;
}
/**
* 同步 CMS仅当前 tenant
*/
public AiKbIngestResult syncCms(Integer tenantId) {
if (tenantId == null) {
throw new BusinessException("tenantId 不能为空");
}
// 仅同步“已发布且未删除”的文章
List<CmsArticle> articles = cmsArticleService.list(new LambdaQueryWrapper<CmsArticle>()
.eq(CmsArticle::getTenantId, tenantId)
.eq(CmsArticle::getDeleted, 0)
.eq(CmsArticle::getStatus, 0));
if (articles == null || articles.isEmpty()) {
AiKbIngestResult r = new AiKbIngestResult();
r.setUpdatedDocuments(0);
r.setSkippedDocuments(0);
r.setChunks(0);
return r;
}
Set<Integer> articleIds = articles.stream().map(CmsArticle::getArticleId).collect(Collectors.toSet());
List<CmsArticleContent> contents = cmsArticleContentService.list(new LambdaQueryWrapper<CmsArticleContent>()
.in(CmsArticleContent::getArticleId, articleIds));
Map<Integer, CmsArticleContent> contentByArticle = contents.stream()
.collect(Collectors.toMap(
CmsArticleContent::getArticleId,
c -> c,
(a, b) -> (a.getCreateTime() != null && b.getCreateTime() != null && a.getCreateTime().isAfter(b.getCreateTime())) ? a : b
));
int updatedDocs = 0;
int skippedDocs = 0;
int totalChunks = 0;
for (CmsArticle a : articles) {
CmsArticleContent c = contentByArticle.get(a.getArticleId());
String raw = "";
if (a.getOverview() != null) {
raw += a.getOverview() + "\n";
}
if (c != null && c.getContent() != null) {
raw += c.getContent();
}
String text = a.getTitle() + "\n" + AiTextUtil.stripHtml(raw);
text = AiTextUtil.normalizeWhitespace(text);
if (StrUtil.isBlank(text)) {
continue;
}
String hash = AiTextUtil.sha256(text);
AiKbDocument existing = documentService.getOne(new LambdaQueryWrapper<AiKbDocument>()
.eq(AiKbDocument::getTenantId, tenantId)
.eq(AiKbDocument::getSourceType, "cms")
.eq(AiKbDocument::getSourceId, a.getArticleId())
.last("limit 1"));
if (existing != null && StrUtil.equals(existing.getContentHash(), hash)) {
skippedDocs++;
continue;
}
AiKbDocument doc;
if (existing == null) {
doc = new AiKbDocument();
doc.setTitle(a.getTitle());
doc.setSourceType("cms");
doc.setSourceId(a.getArticleId());
doc.setSourceRef(a.getCode());
doc.setContentHash(hash);
doc.setStatus(0);
doc.setTenantId(tenantId);
doc.setCreateTime(LocalDateTime.now());
doc.setUpdateTime(LocalDateTime.now());
documentService.save(doc);
} else {
doc = existing;
doc.setTitle(a.getTitle());
doc.setSourceRef(a.getCode());
doc.setContentHash(hash);
doc.setUpdateTime(LocalDateTime.now());
documentService.updateById(doc);
// 重新入库:先删除旧 chunk
chunkService.remove(new LambdaQueryWrapper<AiKbChunk>().eq(AiKbChunk::getDocumentId, doc.getDocumentId()));
}
int chunks = ingestChunks(doc, text);
totalChunks += chunks;
updatedDocs++;
}
AiKbIngestResult r = new AiKbIngestResult();
r.setUpdatedDocuments(updatedDocs);
r.setSkippedDocuments(skippedDocs);
r.setChunks(totalChunks);
return r;
}
public AiKbQueryResult query(Integer tenantId, AiKbQueryRequest request) {
if (tenantId == null) {
throw new BusinessException("tenantId 不能为空");
}
if (request == null || StrUtil.isBlank(request.getQuery())) {
throw new BusinessException("query 不能为空");
}
int topK = request.getTopK() != null ? request.getTopK() : props.getRagTopK();
topK = Math.max(1, Math.min(20, topK));
float[] qEmb = embedding(request.getQuery());
float qNorm = l2(qEmb);
if (qNorm == 0f) {
throw new BusinessException("query embedding 为空");
}
// MVP按 tenant 拉取最新 N 条候选 chunk再做余弦相似度排序
List<AiKbChunk> candidates = chunkService.list(new LambdaQueryWrapper<AiKbChunk>()
.eq(AiKbChunk::getTenantId, tenantId)
.orderByDesc(AiKbChunk::getId)
.last("limit " + props.getRagMaxCandidates()));
PriorityQueue<AiKbHit> pq = new PriorityQueue<>(Comparator.comparingDouble(h -> h.getScore() == null ? -1d : h.getScore()));
for (AiKbChunk c : candidates) {
if (StrUtil.isBlank(c.getEmbedding())) {
continue;
}
float[] cEmb = parseEmbedding(c.getEmbedding());
if (cEmb == null || cEmb.length == 0) {
continue;
}
Double cNormD = c.getEmbeddingNorm();
float cNorm = cNormD == null ? l2(cEmb) : cNormD.floatValue();
if (cNorm == 0f) {
continue;
}
double score = dot(qEmb, cEmb) / (qNorm * cNorm);
AiKbHit hit = new AiKbHit();
hit.setChunkId(c.getChunkId());
hit.setDocumentId(c.getDocumentId());
hit.setTitle(StrUtil.blankToDefault(c.getTitle(), ""));
hit.setScore(score);
// 返回给前端时避免过长
hit.setContent(clip(c.getContent(), 900));
if (pq.size() < topK) {
pq.add(hit);
} else if (hit.getScore() != null && hit.getScore() > pq.peek().getScore()) {
pq.poll();
pq.add(hit);
}
}
List<AiKbHit> hits = new ArrayList<>(pq);
hits.sort((a, b) -> Double.compare(b.getScore(), a.getScore()));
AiKbQueryResult r = new AiKbQueryResult();
r.setHits(hits);
return r;
}
public AiKbAskResult ask(Integer tenantId, AiKbAskRequest request) {
if (request == null || StrUtil.isBlank(request.getQuestion())) {
throw new BusinessException("question 不能为空");
}
AiKbQueryRequest q = new AiKbQueryRequest();
q.setQuery(request.getQuestion());
q.setTopK(request.getTopK());
AiKbQueryResult retrieval = query(tenantId, q);
String context = buildContext(retrieval);
String userPrompt = "上下文资料:\n" + context + "\n\n用户问题\n" + request.getQuestion();
AiChatRequest chatReq = new AiChatRequest();
chatReq.setMessages(Arrays.asList(
new AiMessage("system", AiPrompts.SYSTEM_SUPPORT),
new AiMessage("user", userPrompt)
));
AiChatResult chat = aiChatService.chat(chatReq);
AiKbAskResult r = new AiKbAskResult();
r.setAnswer(chat.getContent());
r.setRetrieval(retrieval);
return r;
}
private int ingestChunks(AiKbDocument doc, String text) {
List<String> chunks = AiTextUtil.chunkText(text, props.getRagChunkSize(), props.getRagChunkOverlap());
if (chunks.isEmpty()) {
return 0;
}
LocalDateTime now = LocalDateTime.now();
List<CompletableFuture<AiKbChunk>> futures = new ArrayList<>(chunks.size());
for (int i = 0; i < chunks.size(); i++) {
final int idx = i;
final String chunkText = chunks.get(i);
futures.add(CompletableFuture.supplyAsync(() -> {
OllamaEmbeddingResponse emb = ollamaClient.embedding(chunkText);
if (emb == null || emb.getEmbedding() == null || emb.getEmbedding().isEmpty()) {
throw new BusinessException("embedding 生成失败");
}
float[] v = toFloat(emb.getEmbedding());
float norm = l2(v);
AiKbChunk c = new AiKbChunk();
c.setDocumentId(doc.getDocumentId());
c.setChunkId(UUID.randomUUID().toString().replace("-", ""));
c.setChunkIndex(idx);
c.setTitle(doc.getTitle());
c.setContent(chunkText);
c.setContentHash(AiTextUtil.sha256(chunkText));
c.setEmbedding(JSONUtil.toJSONString(emb.getEmbedding()));
c.setEmbeddingNorm((double) norm);
c.setTenantId(doc.getTenantId());
c.setCreateTime(now);
c.setDeleted(0);
return c;
}, pool()));
}
List<AiKbChunk> entities = futures.stream().map(f -> {
try {
return f.get(10, TimeUnit.MINUTES);
} catch (Exception e) {
throw new BusinessException("embedding 批处理失败: " + e.getMessage());
}
}).collect(Collectors.toList());
chunkService.saveBatch(entities);
return entities.size();
}
private String extractText(MultipartFile file) {
try {
String contentType = file.getContentType();
String filename = file.getOriginalFilename();
// 优先:对纯文本直接读 UTF-8
if ((contentType != null && contentType.startsWith("text/"))
|| (filename != null && (filename.endsWith(".txt") || filename.endsWith(".md") || filename.endsWith(".html") || filename.endsWith(".htm")))) {
return AiTextUtil.normalizeWhitespace(new String(file.getBytes(), StandardCharsets.UTF_8));
}
// 尝试用 tika 解析(注意:当前依赖为 tika-core解析能力有限
String parsed = tika.parseToString(file.getInputStream());
return AiTextUtil.normalizeWhitespace(parsed);
} catch (Exception e) {
return "";
}
}
private float[] embedding(String text) {
OllamaEmbeddingResponse emb = ollamaClient.embedding(text);
if (emb == null || emb.getEmbedding() == null || emb.getEmbedding().isEmpty()) {
throw new BusinessException("embedding 生成失败");
}
return toFloat(emb.getEmbedding());
}
private float[] parseEmbedding(String json) {
try {
// embedding 是一维数组,存储为 JSON 文本
double[] d = ObjectUtil.isEmpty(json) ? null : objectMapper.readValue(json, double[].class);
if (d == null || d.length == 0) {
return null;
}
float[] f = new float[d.length];
for (int i = 0; i < d.length; i++) {
f[i] = (float) d[i];
}
return f;
} catch (Exception e) {
return null;
}
}
private static float[] toFloat(List<Double> v) {
float[] out = new float[v.size()];
for (int i = 0; i < v.size(); i++) {
Double d = v.get(i);
out[i] = d == null ? 0f : d.floatValue();
}
return out;
}
private static float dot(float[] a, float[] b) {
int n = Math.min(a.length, b.length);
double s = 0d;
for (int i = 0; i < n; i++) {
s += (double) a[i] * (double) b[i];
}
return (float) s;
}
private static float l2(float[] a) {
double s = 0d;
for (float v : a) {
s += (double) v * (double) v;
}
return (float) Math.sqrt(s);
}
private static String clip(String s, int max) {
if (s == null) {
return "";
}
if (s.length() <= max) {
return s;
}
return s.substring(0, max) + "...";
}
private static String buildContext(AiKbQueryResult retrieval) {
if (retrieval == null || retrieval.getHits() == null || retrieval.getHits().isEmpty()) {
return "(无)";
}
StringBuilder sb = new StringBuilder();
for (AiKbHit h : retrieval.getHits()) {
sb.append("[source:").append(h.getChunkId()).append("] ");
if (StrUtil.isNotBlank(h.getTitle())) {
sb.append(h.getTitle()).append("\n");
}
sb.append(h.getContent()).append("\n\n");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,46 @@
package com.gxwebsoft.ai.service;
import com.gxwebsoft.ai.dto.AiShopMetricsRow;
import com.gxwebsoft.ai.mapper.AiShopAnalyticsMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class AiShopAnalyticsService {
@Resource
private AiShopAnalyticsMapper mapper;
public List<AiShopMetricsRow> queryTenantDaily(Integer tenantId, LocalDate startDate, LocalDate endDateInclusive) {
LocalDateTime start = startDate.atStartOfDay();
LocalDateTime endExclusive = endDateInclusive.plusDays(1).atStartOfDay();
List<AiShopMetricsRow> rows = mapper.queryMetrics(tenantId, start, endExclusive);
if (rows == null) {
return null;
}
for (AiShopMetricsRow r : rows) {
long orderCnt = r.getOrderCnt() == null ? 0L : r.getOrderCnt();
long paidCnt = r.getPaidOrderCnt() == null ? 0L : r.getPaidOrderCnt();
BigDecimal gmv = r.getGmv() == null ? BigDecimal.ZERO : r.getGmv();
if (paidCnt > 0) {
r.setAov(gmv.divide(BigDecimal.valueOf(paidCnt), 4, RoundingMode.HALF_UP));
} else {
r.setAov(BigDecimal.ZERO);
}
if (orderCnt > 0) {
r.setPayRate(BigDecimal.valueOf(paidCnt)
.divide(BigDecimal.valueOf(orderCnt), 4, RoundingMode.HALF_UP));
} else {
r.setPayRate(BigDecimal.ZERO);
}
}
return rows;
}
}

View File

@@ -0,0 +1,12 @@
package com.gxwebsoft.ai.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.ai.entity.AiKbChunk;
import com.gxwebsoft.ai.mapper.AiKbChunkMapper;
import com.gxwebsoft.ai.service.AiKbChunkService;
import org.springframework.stereotype.Service;
@Service
public class AiKbChunkServiceImpl extends ServiceImpl<AiKbChunkMapper, AiKbChunk> implements AiKbChunkService {
}

View File

@@ -0,0 +1,12 @@
package com.gxwebsoft.ai.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.ai.entity.AiKbDocument;
import com.gxwebsoft.ai.mapper.AiKbDocumentMapper;
import com.gxwebsoft.ai.service.AiKbDocumentService;
import org.springframework.stereotype.Service;
@Service
public class AiKbDocumentServiceImpl extends ServiceImpl<AiKbDocumentMapper, AiKbDocument> implements AiKbDocumentService {
}

View File

@@ -0,0 +1,100 @@
package com.gxwebsoft.ai.util;
import cn.hutool.core.util.StrUtil;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
public class AiTextUtil {
private AiTextUtil() {
}
public static String sha256(String s) {
if (s == null) {
return null;
}
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] dig = md.digest(s.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(dig.length * 2);
for (byte b : dig) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 很轻量的 HTML 转纯文本(不追求完美,只用于知识库入库前清洗)。
*/
public static String stripHtml(String html) {
if (StrUtil.isBlank(html)) {
return "";
}
String s = html;
s = s.replaceAll("(?is)<script[^>]*>.*?</script>", " ");
s = s.replaceAll("(?is)<style[^>]*>.*?</style>", " ");
s = s.replaceAll("(?is)<br\\s*/?>", "\n");
s = s.replaceAll("(?is)</p\\s*>", "\n");
s = s.replaceAll("(?is)<[^>]+>", " ");
// 常见 HTML 实体最小处理
s = s.replace("&nbsp;", " ");
s = s.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&").replace("&quot;", "\"");
return normalizeWhitespace(s);
}
public static String normalizeWhitespace(String s) {
if (s == null) {
return "";
}
// 合并空白,保留换行用于 chunking
String x = s.replace("\r", "\n");
x = x.replaceAll("[\\t\\f\\u000B]+", " ");
x = x.replaceAll("[ ]{2,}", " ");
x = x.replaceAll("\\n{3,}", "\n\n");
return x.trim();
}
/**
* 按字符数切分,并做固定 overlap。
*/
public static List<String> chunkText(String text, int chunkSize, int overlap) {
String s = normalizeWhitespace(text);
List<String> out = new ArrayList<>();
if (StrUtil.isBlank(s)) {
return out;
}
if (chunkSize <= 0) {
chunkSize = 800;
}
if (overlap < 0) {
overlap = 0;
}
if (overlap >= chunkSize) {
overlap = Math.max(0, chunkSize / 5);
}
int n = s.length();
int start = 0;
while (start < n) {
int end = Math.min(n, start + chunkSize);
String chunk = s.substring(start, end).trim();
if (!chunk.isEmpty()) {
out.add(chunk);
}
if (end >= n) {
break;
}
start = end - overlap;
if (start < 0) {
start = 0;
}
}
return out;
}
}

View File

@@ -92,6 +92,11 @@ public class ConfigProperties {
*/
private String serverUrl;
/**
* API网关地址
*/
private String apiUrl;
/**
* 阿里云存储 OSS
* Endpoint

View File

@@ -50,15 +50,26 @@ public class EnvironmentAwarePaymentService {
return null;
}
// 根据环境调整回调地址
// 开发/测试环境允许强制覆盖,方便本地联调。
if (isDevelopmentEnvironment()) {
Payment envPayment = clonePayment(payment);
String notifyUrl = getEnvironmentNotifyUrl();
log.info("环境感知支付配置(开发/测试) - 环境: {}, 原始回调: {}, 覆盖后回调: {}",
activeProfile, payment.getNotifyUrl(), notifyUrl);
envPayment.setNotifyUrl(notifyUrl);
return envPayment;
}
// 生产/其它环境:优先使用数据库中的 notifyUrl多租户域名可能不同
if (payment.getNotifyUrl() != null && !payment.getNotifyUrl().trim().isEmpty()) {
return payment;
}
// 数据库未配置时才兜底使用环境配置。
Payment envPayment = clonePayment(payment);
String notifyUrl = getEnvironmentNotifyUrl();
log.info("环境感知支付配置 - 环境: {}, 原始回调: {}, 调整后回调: {}",
activeProfile, payment.getNotifyUrl(), notifyUrl);
log.info("环境感知支付配置(兜底) - 环境: {}, 原始回调为空,兜底回调: {}", activeProfile, notifyUrl);
envPayment.setNotifyUrl(notifyUrl);
return envPayment;
}

View File

@@ -42,7 +42,6 @@ import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeUnit;
import static com.gxwebsoft.common.core.constants.PlatformConstants.MP_WEIXIN;
import static com.gxwebsoft.common.core.constants.RedisConstants.ACCESS_TOKEN_KEY;
@@ -246,27 +245,30 @@ public class WxLoginController extends BaseController {
* @param userParam 需要传微信凭证code
*/
private String getPhoneByCode(UserParam userParam) {
// 获取手机号码
String apiUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + getAccessToken();
HashMap<String, Object> paramMap = new HashMap<>();
if (StrUtil.isBlank(userParam.getCode())) {
throw new BusinessException("code不能为空");
}
paramMap.put("code", userParam.getCode());
// 执行post请求
String post = HttpUtil.post(apiUrl, JSON.toJSONString(paramMap));
JSONObject json = JSON.parseObject(post);
if (json.get("errcode").equals(0)) {
JSONObject phoneInfo = JSON.parseObject(json.getString("phone_info"));
// 微信用户的手机号码
final String phoneNumber = phoneInfo.getString("phoneNumber");
// 验证手机号码
// if (userParam.getNotVerifyPhone() == null && !Validator.isMobile(phoneNumber)) {
// String key = ACCESS_TOKEN_KEY.concat(":").concat(getTenantId().toString());
// redisTemplate.delete(key);
// throw new BusinessException("手机号码格式不正确");
// }
return phoneNumber;
// access_token 失效/过期时自动刷新并重试一次
for (int attempt = 0; attempt < 2; attempt++) {
String accessToken = (attempt == 0) ? getAccessToken(false) : getAccessToken(true);
String apiUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken;
String post = HttpUtil.post(apiUrl, JSON.toJSONString(paramMap));
JSONObject json = JSON.parseObject(post);
Integer errcode = json.getInteger("errcode");
if (errcode != null && errcode.equals(0)) {
JSONObject phoneInfo = JSON.parseObject(json.getString("phone_info"));
return phoneInfo.getString("phoneNumber");
}
if (errcode != null && (errcode == 40001 || errcode == 42001 || errcode == 40014)) {
continue;
}
return null;
}
return null;
}
@@ -285,6 +287,10 @@ public class WxLoginController extends BaseController {
* <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html">...</a>
*/
public String getAccessToken() {
return getAccessToken(false);
}
private String getAccessToken(boolean forceRefresh) {
Integer tenantId = getTenantId();
String key = ACCESS_TOKEN_KEY.concat(":").concat(tenantId.toString());
@@ -294,9 +300,13 @@ public class WxLoginController extends BaseController {
throw new BusinessException("请先配置小程序");
}
if (forceRefresh) {
redisTemplate.delete(key);
}
// 从缓存获取access_token
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
if (!forceRefresh && value != null) {
// 解析access_token
JSONObject response = JSON.parseObject(value);
String accessToken = response.getString("access_token");
@@ -305,23 +315,38 @@ public class WxLoginController extends BaseController {
}
}
// 微信获取凭证接口
String apiUrl = "https://api.weixin.qq.com/cgi-bin/token";
// 组装url参数
String url = apiUrl.concat("?grant_type=client_credential")
.concat("&appid=").concat(setting.getString("appId"))
.concat("&secret=").concat(setting.getString("appSecret"));
String appId = setting.getString("appId");
String appSecret = setting.getString("appSecret");
// 执行get请求
String result = HttpUtil.get(url);
// 解析access_token
// 微信稳定版获取凭证接口避免并发刷新导致旧token失效
String apiUrl = "https://api.weixin.qq.com/cgi-bin/stable_token";
JSONObject reqBody = new JSONObject();
reqBody.put("grant_type", "client_credential");
reqBody.put("appid", appId);
reqBody.put("secret", appSecret);
reqBody.put("force_refresh", forceRefresh);
String result = HttpRequest.post(apiUrl)
.header("Content-Type", "application/json")
.body(reqBody.toJSONString())
.execute()
.body();
JSONObject response = JSON.parseObject(result);
if (response.getString("access_token") != null) {
// 存入缓存
redisTemplate.opsForValue().set(key, result, 7000L, TimeUnit.SECONDS);
return response.getString("access_token");
Integer errcode = response.getInteger("errcode");
if (errcode != null && errcode != 0) {
throw new BusinessException("获取access_token失败: " + response.getString("errmsg") + " (errcode: " + errcode + ")");
}
throw new BusinessException("小程序配置不正确");
String accessToken = response.getString("access_token");
Integer expiresIn = response.getInteger("expires_in");
if (accessToken != null) {
long ttlSeconds = Math.max((expiresIn != null ? expiresIn : 7200) - 300L, 60L);
redisTemplate.opsForValue().set(key, result, ttlSeconds, TimeUnit.SECONDS);
return accessToken;
}
throw new BusinessException("获取access_token失败: " + result);
}
@Operation(summary = "获取微信openId并更新")
@@ -576,23 +601,29 @@ public class WxLoginController extends BaseController {
throw new IOException("小程序配置不完整,缺少 appId 或 appSecret");
}
HttpUrl url = HttpUrl.parse("https://api.weixin.qq.com/cgi-bin/token")
.newBuilder()
.addQueryParameter("grant_type", "client_credential")
.addQueryParameter("appid", appId)
.addQueryParameter("secret", appSecret)
.build();
HttpUrl url = HttpUrl.parse("https://api.weixin.qq.com/cgi-bin/stable_token").newBuilder().build();
var root = om.createObjectNode();
root.put("grant_type", "client_credential");
root.put("appid", appId);
root.put("secret", appSecret);
root.put("force_refresh", false);
Request req = new Request.Builder().url(url).get().build();
okhttp3.RequestBody reqBody = okhttp3.RequestBody.create(
root.toString(), MediaType.parse("application/json; charset=utf-8"));
Request req = new Request.Builder().url(url).post(reqBody).build();
try (Response resp = http.newCall(req).execute()) {
String body = resp.body().string();
JsonNode json = om.readTree(body);
if (json.has("errcode") && json.get("errcode").asInt() != 0) {
throw new IOException("Get access_token failed: " + body);
}
if (json.has("access_token")) {
String token = json.get("access_token").asText();
long expiresIn = json.get("expires_in").asInt(7200);
// 缓存完整的JSON响应与其他方法保持一致
redisUtil.set(key, body, expiresIn, TimeUnit.SECONDS);
long ttlSeconds = Math.max(expiresIn - 300L, 60L);
redisUtil.set(key, body, ttlSeconds, TimeUnit.SECONDS);
tokenExpireEpoch = now + expiresIn;
System.out.println("获取新的access_token成功(Local)租户ID: " + tenantId);
return token;
@@ -686,7 +717,7 @@ public class WxLoginController extends BaseController {
// 获取当前线程的租户ID
Integer tenantId = getTenantId();
if (tenantId == null) {
tenantId = 10550; // 默认租户
tenantId = 10584; // 默认租户
}
System.out.println("=== 开始调试获取AccessToken租户ID: " + tenantId + " ===");
@@ -748,14 +779,14 @@ public class WxLoginController extends BaseController {
}
}
// 如果无法解析默认使用租户10550
System.out.println("无法解析scene参数使用默认租户ID: 10550");
return 10550;
// 如果无法解析默认使用租户10584
System.out.println("无法解析scene参数使用默认租户ID: 10584");
return 10584;
} catch (Exception e) {
System.err.println("解析scene参数异常: " + e.getMessage());
// 出现异常时默认使用租户10550
return 10550;
// 出现异常时默认使用租户10584
return 10584;
}
}
@@ -789,10 +820,21 @@ public class WxLoginController extends BaseController {
String appId = wxConfig.getString("appId");
String appSecret = wxConfig.getString("appSecret");
String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret;
String apiUrl = "https://api.weixin.qq.com/cgi-bin/stable_token";
System.out.println("调用微信API获取token - 租户ID: " + tenantId + ", AppID: " + (appId != null ? appId.substring(0, Math.min(8, appId.length())) + "..." : "null"));
System.out.println("微信API请求URL: " + apiUrl.replaceAll("secret=[^&]*", "secret=***"));
String result = HttpUtil.get(apiUrl);
System.out.println("微信API请求URL: " + apiUrl);
JSONObject reqBody = new JSONObject();
reqBody.put("grant_type", "client_credential");
reqBody.put("appid", appId);
reqBody.put("secret", appSecret);
reqBody.put("force_refresh", false);
String result = HttpRequest.post(apiUrl)
.header("Content-Type", "application/json")
.body(reqBody.toJSONString())
.execute()
.body();
System.out.println("微信API响应: " + result);
JSONObject json = JSON.parseObject(result);
@@ -800,14 +842,15 @@ public class WxLoginController extends BaseController {
if (json.containsKey("errcode")) {
Integer errcode = json.getInteger("errcode");
String errmsg = json.getString("errmsg");
System.err.println("微信API错误 - errcode: " + errcode + ", errmsg: " + errmsg);
if (errcode == 40125) {
throw new RuntimeException("微信AppSecret配置错误请检查并更新正确的AppSecret");
} else if (errcode == 40013) {
throw new RuntimeException("微信AppID配置错误请检查并更新正确的AppID");
} else {
throw new RuntimeException("微信API调用失败: " + errmsg + " (errcode: " + errcode + ")");
if (errcode != null && errcode != 0) {
System.err.println("微信API错误 - errcode: " + errcode + ", errmsg: " + errmsg);
if (errcode == 40125) {
throw new RuntimeException("微信AppSecret配置错误请检查并更新正确的AppSecret");
} else if (errcode == 40013) {
throw new RuntimeException("微信AppID配置错误请检查并更新正确的AppID");
} else {
throw new RuntimeException("微信API调用失败: " + errmsg + " (errcode: " + errcode + ")");
}
}
}
@@ -816,7 +859,8 @@ public class WxLoginController extends BaseController {
Integer expiresIn = json.getInteger("expires_in");
// 缓存access_token存储完整JSON响应与getAccessToken方法保持一致
redisUtil.set(key, result, (long) (expiresIn - 300), TimeUnit.SECONDS);
long ttlSeconds = Math.max((expiresIn != null ? expiresIn : 7200) - 300L, 60L);
redisUtil.set(key, result, ttlSeconds, TimeUnit.SECONDS);
System.out.println("获取新的access_token成功租户ID: " + tenantId);
return accessToken;

View File

@@ -102,7 +102,7 @@
AND a.deleted = 0
</if>
<if test="param.roleId != null">
AND a.user_id IN (SELECT user_id FROM sys_user_role WHERE role_id=#{param.roleId})
AND a.user_id IN (SELECT user_id FROM gxwebsoft_core.sys_user_role WHERE role_id=#{param.roleId})
</if>
<if test="param.userIds != null">
AND a.user_id IN
@@ -259,6 +259,7 @@
LEFT JOIN gxwebsoft_core.sys_user_referee h ON a.user_id = h.user_id and h.deleted = 0
WHERE a.user_id = #{userId}
AND a.deleted = 0
LIMIT 1
</select>
</mapper>

View File

@@ -1,17 +1,23 @@
package com.gxwebsoft.credit.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.toolkit.SqlRunner;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.service.CreditCompanyService;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.CollectionUtils;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.HashMap;
@@ -48,6 +54,66 @@ public class BatchImportSupport {
return requiresNewTx.execute(status -> supplier.get());
}
/**
* 硬删除(物理删除),用于替代 MyBatis-Plus 的 @TableLogic 逻辑删除。
*
* <p>注意SQL 仍会经过 MyBatis-Plus 拦截器(如 TenantLine用于保证租户隔离。</p>
*/
public boolean hardRemoveById(Class<?> entityClass, Serializable id) {
if (entityClass == null || id == null) {
return false;
}
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
if (tableInfo == null) {
throw new IllegalArgumentException("MyBatis-Plus TableInfo not found for entityClass=" + entityClass.getName());
}
String tableName = tableInfo.getTableName();
String keyColumn = tableInfo.getKeyColumn();
if (keyColumn == null || keyColumn.trim().isEmpty()) {
keyColumn = "id";
}
String sql = "DELETE FROM " + tableName + " WHERE " + keyColumn + " = {0}";
return SqlRunner.db().delete(sql, id);
}
/**
* 硬删除(物理删除)- 批量
*/
public boolean hardRemoveByIds(Class<?> entityClass, List<? extends Serializable> ids) {
if (entityClass == null || CollectionUtils.isEmpty(ids)) {
return false;
}
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
if (tableInfo == null) {
throw new IllegalArgumentException("MyBatis-Plus TableInfo not found for entityClass=" + entityClass.getName());
}
String tableName = tableInfo.getTableName();
String keyColumn = tableInfo.getKeyColumn();
if (keyColumn == null || keyColumn.trim().isEmpty()) {
keyColumn = "id";
}
// Keep IN-list size under common DB/driver limits.
final int chunkSize = 900;
boolean anyDeleted = false;
for (int i = 0; i < ids.size(); i += chunkSize) {
List<? extends Serializable> chunk = ids.subList(i, Math.min(ids.size(), i + chunkSize));
if (CollectionUtils.isEmpty(chunk)) {
continue;
}
StringBuilder inPlaceholders = new StringBuilder();
for (int j = 0; j < chunk.size(); j++) {
if (j > 0) {
inPlaceholders.append(",");
}
inPlaceholders.append("{").append(j).append("}");
}
String sql = "DELETE FROM " + tableName + " WHERE " + keyColumn + " IN (" + inPlaceholders + ")";
anyDeleted = SqlRunner.db().delete(sql, chunk.toArray()) || anyDeleted;
}
return anyDeleted;
}
public static final class CompanyIdRefreshStats {
public final boolean anyDataRead;
public final int updated;
@@ -112,6 +178,43 @@ public class BatchImportSupport {
nameGetter);
}
/**
* 按企业名称匹配 CreditCompany(name / matchName) 并回填 companyId + companyName。
*
* <p>companyNameSetter 为空时等价于仅回填 companyId。</p>
*/
public <T> CompanyIdRefreshStats refreshCompanyIdByCompanyName(IService<T> service,
CreditCompanyService creditCompanyService,
Integer currentTenantId,
Boolean onlyNull,
Integer limit,
SFunction<T, Integer> idGetter,
BiConsumer<T, Integer> idSetter,
SFunction<T, String> nameGetter,
SFunction<T, Integer> companyIdGetter,
BiConsumer<T, Integer> companyIdSetter,
BiConsumer<T, String> companyNameSetter,
SFunction<T, Boolean> hasDataGetter,
BiConsumer<T, Boolean> hasDataSetter,
SFunction<T, Integer> tenantIdGetter,
Supplier<T> patchFactory) {
return refreshCompanyIdByCompanyNames(service,
creditCompanyService,
currentTenantId,
onlyNull,
limit,
idGetter,
idSetter,
companyIdGetter,
companyIdSetter,
companyNameSetter,
hasDataGetter,
hasDataSetter,
tenantIdGetter,
patchFactory,
nameGetter);
}
/**
* 按多列“当事人/企业名称”匹配 CreditCompany(name / matchName) 并回填 companyId。
*
@@ -134,6 +237,44 @@ public class BatchImportSupport {
SFunction<T, Integer> tenantIdGetter,
Supplier<T> patchFactory,
SFunction<T, String>... nameGetters) {
return refreshCompanyIdByCompanyNames(service,
creditCompanyService,
currentTenantId,
onlyNull,
limit,
idGetter,
idSetter,
companyIdGetter,
companyIdSetter,
null,
hasDataGetter,
hasDataSetter,
tenantIdGetter,
patchFactory,
nameGetters);
}
/**
* 按多列“当事人/企业名称”匹配 CreditCompany(name / matchName) 并回填 companyId + companyName。
*
* <p>companyNameSetter 为空时等价于仅回填 companyId。</p>
*/
@SafeVarargs
public final <T> CompanyIdRefreshStats refreshCompanyIdByCompanyNames(IService<T> service,
CreditCompanyService creditCompanyService,
Integer currentTenantId,
Boolean onlyNull,
Integer limit,
SFunction<T, Integer> idGetter,
BiConsumer<T, Integer> idSetter,
SFunction<T, Integer> companyIdGetter,
BiConsumer<T, Integer> companyIdSetter,
BiConsumer<T, String> companyNameSetter,
SFunction<T, Boolean> hasDataGetter,
BiConsumer<T, Boolean> hasDataSetter,
SFunction<T, Integer> tenantIdGetter,
Supplier<T> patchFactory,
SFunction<T, String>... nameGetters) {
boolean onlyNullFlag = (onlyNull == null) || Boolean.TRUE.equals(onlyNull);
if (nameGetters == null || nameGetters.length == 0) {
@@ -214,6 +355,8 @@ public class BatchImportSupport {
// 3.1) 查询当前租户下的 companyId 映射
LinkedHashMap<String, Integer> companyIdByName = new LinkedHashMap<>();
LinkedHashMap<String, Integer> ambiguousByName = new LinkedHashMap<>();
// For display: prefer matchName (normalized) then name.
HashMap<Integer, String> companyNameById = new HashMap<>();
LinkedHashSet<String> nameSet = new LinkedHashSet<>();
for (T row : tenantRows) {
if (row == null) {
@@ -243,6 +386,13 @@ public class BatchImportSupport {
if (c == null || c.getId() == null) {
continue;
}
String displayName = c.getMatchName();
if (displayName == null || displayName.trim().isEmpty()) {
displayName = c.getName();
}
if (displayName != null && !displayName.trim().isEmpty()) {
companyNameById.putIfAbsent(c.getId(), displayName.trim());
}
addCompanyNameMapping(companyIdByName, ambiguousByName, normalizeCompanyName(c.getName()), c.getId());
addCompanyNameMapping(companyIdByName, ambiguousByName, normalizeCompanyName(c.getMatchName()), c.getId());
}
@@ -315,6 +465,12 @@ public class BatchImportSupport {
T patch = patchFactory.get();
idSetter.accept(patch, id);
companyIdSetter.accept(patch, companyId);
if (companyNameSetter != null) {
String companyName = companyNameById.get(companyId);
if (companyName != null && !companyName.trim().isEmpty()) {
companyNameSetter.accept(patch, companyName.trim());
}
}
hasDataSetter.accept(patch, Boolean.TRUE);
updates.add(patch);
if (updates.size() >= batchSize) {
@@ -973,6 +1129,95 @@ public class BatchImportSupport {
}
}
/**
* Insert-only batch persist with "unique index" duplicate handling:
* - try saveBatch
* - on failure, fallback to row-by-row save
* - if a row hits duplicate-key constraint, add a friendly error and continue
*
* <p>This intentionally does NOT query the database for dedup/upsert.</p>
*/
public <T> int persistInsertOnlyChunk(IService<T> service,
List<T> items,
List<Integer> excelRowNumbers,
int batchSize,
Function<T, String> rowLabelGetter,
String messagePrefix,
List<String> errorMessages) {
String prefix = messagePrefix == null ? "" : messagePrefix;
return persistChunkWithFallback(
items,
excelRowNumbers,
() -> {
boolean ok = service.saveBatch(items, batchSize);
if (!ok) {
throw new RuntimeException("批量保存失败");
}
return items.size();
},
(rowItem, rowNumber) -> {
try {
boolean ok = service.save(rowItem);
if (!ok) {
if (errorMessages != null) {
String p = (rowNumber != null && rowNumber > 0) ? (prefix + "" + rowNumber + "行:") : prefix;
errorMessages.add(p + "保存失败");
}
return false;
}
return true;
} catch (DataIntegrityViolationException e) {
if (!isDuplicateKey(e)) {
throw e;
}
if (errorMessages != null) {
String label = null;
if (rowLabelGetter != null && rowItem != null) {
try {
label = rowLabelGetter.apply(rowItem);
} catch (Exception ignore) {
// ignore label extraction failures
}
}
if (label != null) {
label = label.trim();
}
String what = (label == null || label.isEmpty()) ? "数据" : ("" + label + "");
String p = (rowNumber != null && rowNumber > 0) ? (prefix + "" + rowNumber + "行:") : prefix;
errorMessages.add(p + what + "重复(唯一索引冲突)");
}
return false;
}
},
errorMessages
);
}
/**
* Best-effort duplicate-key detection across common drivers.
*/
public static boolean isDuplicateKey(DataIntegrityViolationException e) {
for (Throwable t = e; t != null; t = t.getCause()) {
if (t instanceof SQLException) {
SQLException se = (SQLException) t;
// MySQL: 1062 Duplicate entry; PostgreSQL/H2: SQLState 23505 unique_violation
if (se.getErrorCode() == 1062) {
return true;
}
if ("23505".equals(se.getSQLState())) {
return true;
}
}
}
Throwable mostSpecificCause = e.getMostSpecificCause();
String message = mostSpecificCause != null ? mostSpecificCause.getMessage() : e.getMessage();
if (message == null) {
return false;
}
String lower = message.toLowerCase();
return lower.contains("duplicate") && lower.contains("key");
}
/**
* 批量失败时降级逐行:允许调用方自定义“成功条数”的计算口径(例如:仅统计 insert 入库条数)。
*

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditAdministrativeLicense;
import com.gxwebsoft.credit.param.CreditAdministrativeLicenseImportParam;
import com.gxwebsoft.credit.param.CreditAdministrativeLicenseParam;
@@ -25,7 +26,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -105,7 +105,7 @@ public class CreditAdministrativeLicenseController extends BaseController {
@Operation(summary = "删除行政许可")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditAdministrativeLicenseService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditAdministrativeLicense.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -138,7 +138,7 @@ public class CreditAdministrativeLicenseController extends BaseController {
@Operation(summary = "批量删除行政许可")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditAdministrativeLicenseService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditAdministrativeLicense.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -171,6 +171,7 @@ public class CreditAdministrativeLicenseController extends BaseController {
CreditAdministrativeLicense::getName,
CreditAdministrativeLicense::getCompanyId,
CreditAdministrativeLicense::setCompanyId,
CreditAdministrativeLicense::setCompanyName,
CreditAdministrativeLicense::getHasData,
CreditAdministrativeLicense::setHasData,
CreditAdministrativeLicense::getTenantId,
@@ -212,6 +213,11 @@ public class CreditAdministrativeLicenseController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCode = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "决定文书/许可编号");
Map<String, String> urlByName = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "决定文书/许可证名称");
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -235,6 +241,9 @@ public class CreditAdministrativeLicenseController extends BaseController {
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
@@ -268,48 +277,13 @@ public class CreditAdministrativeLicenseController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditAdministrativeLicenseService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertByCodeOrName(
creditAdministrativeLicenseService,
chunkItems,
CreditAdministrativeLicense::getId,
CreditAdministrativeLicense::setId,
CreditAdministrativeLicense::getCode,
CreditAdministrativeLicense::getCode,
CreditAdministrativeLicense::getName,
CreditAdministrativeLicense::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditAdministrativeLicenseService.save(rowItem);
if (!saved) {
CreditAdministrativeLicense existing = null;
if (!ImportHelper.isBlank(rowItem.getCode())) {
existing = creditAdministrativeLicenseService.lambdaQuery()
.eq(CreditAdministrativeLicense::getCode, rowItem.getCode())
.one();
}
if (existing == null) {
existing = creditAdministrativeLicenseService.lambdaQuery()
.eq(CreditAdministrativeLicense::getName, rowItem.getName())
.one();
}
if (existing != null) {
rowItem.setId(existing.getId());
if (creditAdministrativeLicenseService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditAdministrativeLicense::getName,
"",
errorMessages
);
chunkItems.clear();
@@ -323,48 +297,13 @@ public class CreditAdministrativeLicenseController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditAdministrativeLicenseService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertByCodeOrName(
creditAdministrativeLicenseService,
chunkItems,
CreditAdministrativeLicense::getId,
CreditAdministrativeLicense::setId,
CreditAdministrativeLicense::getCode,
CreditAdministrativeLicense::getCode,
CreditAdministrativeLicense::getName,
CreditAdministrativeLicense::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditAdministrativeLicenseService.save(rowItem);
if (!saved) {
CreditAdministrativeLicense existing = null;
if (!ImportHelper.isBlank(rowItem.getCode())) {
existing = creditAdministrativeLicenseService.lambdaQuery()
.eq(CreditAdministrativeLicense::getCode, rowItem.getCode())
.one();
}
if (existing == null) {
existing = creditAdministrativeLicenseService.lambdaQuery()
.eq(CreditAdministrativeLicense::getName, rowItem.getName())
.one();
}
if (existing != null) {
rowItem.setId(existing.getId());
if (creditAdministrativeLicenseService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditAdministrativeLicense::getName,
"",
errorMessages
);
}
@@ -384,7 +323,7 @@ public class CreditAdministrativeLicenseController extends BaseController {
/**
* 批量导入历史行政许可(仅解析“历史行政许可”选项卡)
* 规则:优先按编号(code)匹配code 为空时按名称(name)匹配匹配到则覆盖更新recommend++ 记录更新次数)
* 规则:使用数据库唯一索引约束,重复数据不导入
*/
@PreAuthorize("hasAuthority('credit:creditAdministrativeLicense:save')")
@Operation(summary = "批量导入历史行政许可")
@@ -417,9 +356,16 @@ public class CreditAdministrativeLicenseController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCode = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "决定文书/许可编号");
Map<String, String> urlByName = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "决定文书/许可证名称");
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
LinkedHashMap<String, CreditAdministrativeLicense> latestByKey = new LinkedHashMap<>();
LinkedHashMap<String, Integer> latestRowByKey = new LinkedHashMap<>();
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditAdministrativeLicense> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditAdministrativeLicenseImportParam param = list.get(i);
@@ -451,6 +397,9 @@ public class CreditAdministrativeLicenseController extends BaseController {
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
@@ -467,144 +416,42 @@ public class CreditAdministrativeLicenseController extends BaseController {
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
String dedupKey = !ImportHelper.isBlank(item.getCode()) ? ("CODE:" + item.getCode()) : ("NAME:" + item.getName());
latestByKey.put(dedupKey, item);
latestRowByKey.put(dedupKey, excelRowNumber);
if (item.getRecommend() == null) {
item.setRecommend(0);
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditAdministrativeLicenseService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditAdministrativeLicense::getName,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (latestByKey.isEmpty()) {
if (errorMessages.isEmpty()) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
return success("导入完成成功0条失败" + errorMessages.size() + "", errorMessages);
}
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditAdministrativeLicense> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (Map.Entry<String, CreditAdministrativeLicense> entry : latestByKey.entrySet()) {
String dedupKey = entry.getKey();
CreditAdministrativeLicense item = entry.getValue();
Integer rowNo = latestRowByKey.get(dedupKey);
chunkItems.add(item);
chunkRowNumbers.add(rowNo != null ? rowNo : -1);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertByCodeOrNameAndIncrementCounterOnUpdate(
creditAdministrativeLicenseService,
chunkItems,
CreditAdministrativeLicense::getId,
CreditAdministrativeLicense::setId,
CreditAdministrativeLicense::getCode,
CreditAdministrativeLicense::getCode,
CreditAdministrativeLicense::getName,
CreditAdministrativeLicense::getName,
CreditAdministrativeLicense::getRecommend,
CreditAdministrativeLicense::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditAdministrativeLicenseService.save(rowItem);
if (!saved) {
CreditAdministrativeLicense existing = null;
if (!ImportHelper.isBlank(rowItem.getCode())) {
existing = creditAdministrativeLicenseService.lambdaQuery()
.eq(CreditAdministrativeLicense::getCode, rowItem.getCode())
.select(CreditAdministrativeLicense::getId, CreditAdministrativeLicense::getRecommend)
.one();
}
if (existing == null && !ImportHelper.isBlank(rowItem.getName())) {
existing = creditAdministrativeLicenseService.lambdaQuery()
.eq(CreditAdministrativeLicense::getName, rowItem.getName())
.select(CreditAdministrativeLicense::getId, CreditAdministrativeLicense::getRecommend)
.one();
}
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditAdministrativeLicenseService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditAdministrativeLicenseService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertByCodeOrNameAndIncrementCounterOnUpdate(
creditAdministrativeLicenseService,
chunkItems,
CreditAdministrativeLicense::getId,
CreditAdministrativeLicense::setId,
CreditAdministrativeLicense::getCode,
CreditAdministrativeLicense::getCode,
CreditAdministrativeLicense::getName,
CreditAdministrativeLicense::getName,
CreditAdministrativeLicense::getRecommend,
CreditAdministrativeLicense::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditAdministrativeLicenseService.save(rowItem);
if (!saved) {
CreditAdministrativeLicense existing = null;
if (!ImportHelper.isBlank(rowItem.getCode())) {
existing = creditAdministrativeLicenseService.lambdaQuery()
.eq(CreditAdministrativeLicense::getCode, rowItem.getCode())
.select(CreditAdministrativeLicense::getId, CreditAdministrativeLicense::getRecommend)
.one();
}
if (existing == null && !ImportHelper.isBlank(rowItem.getName())) {
existing = creditAdministrativeLicenseService.lambdaQuery()
.eq(CreditAdministrativeLicense::getName, rowItem.getName())
.select(CreditAdministrativeLicense::getId, CreditAdministrativeLicense::getRecommend)
.one();
}
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditAdministrativeLicenseService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditAdministrativeLicense::getName,
"",
errorMessages
);
}

View File

@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditBankruptcyController extends BaseController {
@Operation(summary = "删除破产重整")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditBankruptcyService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditBankruptcy.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditBankruptcyController extends BaseController {
@Operation(summary = "批量删除破产重整")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditBankruptcyService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditBankruptcy.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -196,8 +195,16 @@ public class CreditBankruptcyController extends BaseController {
Set<Integer> touchedCompanyIds = new HashSet<>();
try {
ExcelImportSupport.ImportResult<CreditBankruptcyImportParam> importResult = ExcelImportSupport.readAnySheet(
file, CreditBankruptcyImportParam.class, this::isEmptyImportRow);
// Prefer importing from the explicit tab name "破产重整" when present.
// This avoids accidentally importing from other sheets (e.g. "历史破产重整") in multi-sheet workbooks.
int sheetIndex = ExcelImportSupport.findSheetIndex(file, "破产重整");
ExcelImportSupport.ImportResult<CreditBankruptcyImportParam> importResult;
if (sheetIndex >= 0) {
importResult = ExcelImportSupport.read(file, CreditBankruptcyImportParam.class, this::isEmptyImportRow, sheetIndex);
} else {
// Backward compatible: try any sheet for older templates without the expected tab name.
importResult = ExcelImportSupport.readAnySheet(file, CreditBankruptcyImportParam.class, this::isEmptyImportRow);
}
List<CreditBankruptcyImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows();
@@ -263,38 +270,13 @@ public class CreditBankruptcyController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditBankruptcyService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditBankruptcyService,
chunkItems,
CreditBankruptcy::getId,
CreditBankruptcy::setId,
CreditBankruptcy::getCode,
CreditBankruptcy::getCode,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditBankruptcyService.save(rowItem);
if (!saved) {
CreditBankruptcy existing = creditBankruptcyService.lambdaQuery()
.eq(CreditBankruptcy::getCode, rowItem.getCode())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditBankruptcyService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditBankruptcy::getCode,
"",
errorMessages
);
chunkItems.clear();
@@ -308,38 +290,13 @@ public class CreditBankruptcyController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditBankruptcyService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditBankruptcyService,
chunkItems,
CreditBankruptcy::getId,
CreditBankruptcy::setId,
CreditBankruptcy::getCode,
CreditBankruptcy::getCode,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditBankruptcyService.save(rowItem);
if (!saved) {
CreditBankruptcy existing = creditBankruptcyService.lambdaQuery()
.eq(CreditBankruptcy::getCode, rowItem.getCode())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditBankruptcyService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditBankruptcy::getCode,
"",
errorMessages
);
}
@@ -359,7 +316,7 @@ public class CreditBankruptcyController extends BaseController {
/**
* 批量导入历史破产重整(仅解析“历史破产重整”选项卡)
* 规则:案号/唯一标识相同则覆盖更新recommend++ 记录更新次数);不存在则插入。
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditBankruptcy:save')")
@Operation(summary = "批量导入历史破产重整")
@@ -392,8 +349,10 @@ public class CreditBankruptcyController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCode = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
LinkedHashMap<String, CreditBankruptcy> latestByCode = new LinkedHashMap<>();
LinkedHashMap<String, Integer> latestRowByCode = new LinkedHashMap<>();
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditBankruptcy> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditBankruptcyImportParam param = list.get(i);
@@ -429,121 +388,42 @@ public class CreditBankruptcyController extends BaseController {
item.setDeleted(0);
}
latestByCode.put(item.getCode(), item);
latestRowByCode.put(item.getCode(), excelRowNumber);
if (item.getRecommend() == null) {
item.setRecommend(0);
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditBankruptcyService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditBankruptcy::getCode,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (latestByCode.isEmpty()) {
if (errorMessages.isEmpty()) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
return success("导入完成成功0条失败" + errorMessages.size() + "", errorMessages);
}
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditBankruptcy> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (Map.Entry<String, CreditBankruptcy> entry : latestByCode.entrySet()) {
String code = entry.getKey();
CreditBankruptcy item = entry.getValue();
Integer rowNo = latestRowByCode.get(code);
chunkItems.add(item);
chunkRowNumbers.add(rowNo != null ? rowNo : -1);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditBankruptcyService,
chunkItems,
CreditBankruptcy::getId,
CreditBankruptcy::setId,
CreditBankruptcy::getCode,
CreditBankruptcy::getCode,
CreditBankruptcy::getRecommend,
CreditBankruptcy::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditBankruptcyService.save(rowItem);
if (!saved) {
CreditBankruptcy existing = creditBankruptcyService.lambdaQuery()
.eq(CreditBankruptcy::getCode, rowItem.getCode())
.select(CreditBankruptcy::getId, CreditBankruptcy::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditBankruptcyService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditBankruptcyService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditBankruptcyService,
chunkItems,
CreditBankruptcy::getId,
CreditBankruptcy::setId,
CreditBankruptcy::getCode,
CreditBankruptcy::getCode,
CreditBankruptcy::getRecommend,
CreditBankruptcy::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditBankruptcyService.save(rowItem);
if (!saved) {
CreditBankruptcy existing = creditBankruptcyService.lambdaQuery()
.eq(CreditBankruptcy::getCode, rowItem.getCode())
.select(CreditBankruptcy::getId, CreditBankruptcy::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditBankruptcyService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditBankruptcy::getCode,
"",
errorMessages
);
}

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditBranch;
import com.gxwebsoft.credit.param.CreditBranchImportParam;
import com.gxwebsoft.credit.param.CreditBranchParam;
@@ -104,7 +105,7 @@ public class CreditBranchController extends BaseController {
@Operation(summary = "删除分支机构")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditBranchService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditBranch.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditBranchController extends BaseController {
@Operation(summary = "批量删除分支机构")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditBranchService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditBranch.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditBranchController extends BaseController {
CreditBranch::getName,
CreditBranch::getCompanyId,
CreditBranch::setCompanyId,
CreditBranch::setCompanyName,
CreditBranch::getHasData,
CreditBranch::setHasData,
CreditBranch::getTenantId,
@@ -210,6 +212,11 @@ public class CreditBranchController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "分支机构名称");
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -229,6 +236,9 @@ public class CreditBranchController extends BaseController {
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
@@ -259,38 +269,13 @@ public class CreditBranchController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditBranchService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditBranchService,
chunkItems,
CreditBranch::getId,
CreditBranch::setId,
CreditBranch::getName,
CreditBranch::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditBranchService.save(rowItem);
if (!saved) {
CreditBranch existing = creditBranchService.lambdaQuery()
.eq(CreditBranch::getName, rowItem.getName())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditBranchService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditBranch::getName,
"",
errorMessages
);
chunkItems.clear();
@@ -304,38 +289,13 @@ public class CreditBranchController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditBranchService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditBranchService,
chunkItems,
CreditBranch::getId,
CreditBranch::setId,
CreditBranch::getName,
CreditBranch::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditBranchService.save(rowItem);
if (!saved) {
CreditBranch existing = creditBranchService.lambdaQuery()
.eq(CreditBranch::getName, rowItem.getName())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditBranchService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditBranch::getName,
"",
errorMessages
);
}

View File

@@ -23,10 +23,8 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -101,7 +99,7 @@ public class CreditBreachOfTrustController extends BaseController {
@Operation(summary = "删除失信被执行人")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditBreachOfTrustService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditBreachOfTrust.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -134,7 +132,7 @@ public class CreditBreachOfTrustController extends BaseController {
@Operation(summary = "批量删除失信被执行人")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditBreachOfTrustService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditBreachOfTrust.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -264,38 +262,13 @@ public class CreditBreachOfTrustController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditBreachOfTrustService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditBreachOfTrustService,
chunkItems,
CreditBreachOfTrust::getId,
CreditBreachOfTrust::setId,
CreditBreachOfTrust::getCaseNumber,
CreditBreachOfTrust::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditBreachOfTrustService.save(rowItem);
if (!saved) {
CreditBreachOfTrust existing = creditBreachOfTrustService.lambdaQuery()
.eq(CreditBreachOfTrust::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditBreachOfTrustService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditBreachOfTrust::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
@@ -309,38 +282,13 @@ public class CreditBreachOfTrustController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditBreachOfTrustService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditBreachOfTrustService,
chunkItems,
CreditBreachOfTrust::getId,
CreditBreachOfTrust::setId,
CreditBreachOfTrust::getCaseNumber,
CreditBreachOfTrust::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditBreachOfTrustService.save(rowItem);
if (!saved) {
CreditBreachOfTrust existing = creditBreachOfTrustService.lambdaQuery()
.eq(CreditBreachOfTrust::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditBreachOfTrustService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditBreachOfTrust::getCaseNumber,
"",
errorMessages
);
}
@@ -360,7 +308,7 @@ public class CreditBreachOfTrustController extends BaseController {
/**
* 批量导入历史失信被执行人(仅解析“历史失信被执行人”选项卡)
* 规则:案号相同则覆盖更新recommend++ 记录更新次数);案号不存在则插入。
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditBreachOfTrust:save')")
@Operation(summary = "批量导入历史失信被执行人")
@@ -394,8 +342,10 @@ public class CreditBreachOfTrustController extends BaseController {
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
// 同案号多条:以导入文件中“最后一条”为准(视为最新)
LinkedHashMap<String, CreditBreachOfTrust> latestByCaseNumber = new LinkedHashMap<>();
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>();
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditBreachOfTrust> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditBreachOfTrustImportParam param = list.get(i);
@@ -433,121 +383,42 @@ public class CreditBreachOfTrustController extends BaseController {
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item);
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber);
if (item.getRecommend() == null) {
item.setRecommend(0);
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditBreachOfTrustService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditBreachOfTrust::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (latestByCaseNumber.isEmpty()) {
if (errorMessages.isEmpty()) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
return success("导入完成成功0条失败" + errorMessages.size() + "", errorMessages);
}
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditBreachOfTrust> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (Map.Entry<String, CreditBreachOfTrust> entry : latestByCaseNumber.entrySet()) {
String caseNumber = entry.getKey();
CreditBreachOfTrust item = entry.getValue();
Integer rowNo = latestRowByCaseNumber.get(caseNumber);
chunkItems.add(item);
chunkRowNumbers.add(rowNo != null ? rowNo : -1);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditBreachOfTrustService,
chunkItems,
CreditBreachOfTrust::getId,
CreditBreachOfTrust::setId,
CreditBreachOfTrust::getCaseNumber,
CreditBreachOfTrust::getCaseNumber,
CreditBreachOfTrust::getRecommend,
CreditBreachOfTrust::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditBreachOfTrustService.save(rowItem);
if (!saved) {
CreditBreachOfTrust existing = creditBreachOfTrustService.lambdaQuery()
.eq(CreditBreachOfTrust::getCaseNumber, rowItem.getCaseNumber())
.select(CreditBreachOfTrust::getId, CreditBreachOfTrust::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditBreachOfTrustService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditBreachOfTrustService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditBreachOfTrustService,
chunkItems,
CreditBreachOfTrust::getId,
CreditBreachOfTrust::setId,
CreditBreachOfTrust::getCaseNumber,
CreditBreachOfTrust::getCaseNumber,
CreditBreachOfTrust::getRecommend,
CreditBreachOfTrust::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditBreachOfTrustService.save(rowItem);
if (!saved) {
CreditBreachOfTrust existing = creditBreachOfTrustService.lambdaQuery()
.eq(CreditBreachOfTrust::getCaseNumber, rowItem.getCaseNumber())
.select(CreditBreachOfTrust::getId, CreditBreachOfTrust::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditBreachOfTrustService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditBreachOfTrust::getCaseNumber,
"",
errorMessages
);
}

View File

@@ -104,7 +104,7 @@ public class CreditCaseFilingController extends BaseController {
@Operation(summary = "删除司法大数据")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCaseFilingService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditCaseFiling.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +137,7 @@ public class CreditCaseFilingController extends BaseController {
@Operation(summary = "批量删除司法大数据")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCaseFilingService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditCaseFiling.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -265,38 +265,13 @@ public class CreditCaseFilingController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCaseFilingService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditCaseFilingService,
chunkItems,
CreditCaseFiling::getId,
CreditCaseFiling::setId,
CreditCaseFiling::getCaseNumber,
CreditCaseFiling::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditCaseFilingService.save(rowItem);
if (!saved) {
CreditCaseFiling existing = creditCaseFilingService.lambdaQuery()
.eq(CreditCaseFiling::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditCaseFilingService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditCaseFiling::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
@@ -310,38 +285,13 @@ public class CreditCaseFilingController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCaseFilingService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditCaseFilingService,
chunkItems,
CreditCaseFiling::getId,
CreditCaseFiling::setId,
CreditCaseFiling::getCaseNumber,
CreditCaseFiling::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditCaseFilingService.save(rowItem);
if (!saved) {
CreditCaseFiling existing = creditCaseFilingService.lambdaQuery()
.eq(CreditCaseFiling::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditCaseFilingService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditCaseFiling::getCaseNumber,
"",
errorMessages
);
}
@@ -359,6 +309,134 @@ public class CreditCaseFilingController extends BaseController {
}
}
/**
* 批量导入历史立案信息(仅解析“历史立案信息”选项卡)
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditCaseFiling:save')")
@Operation(summary = "批量导入历史立案信息")
@PostMapping("/import/history")
public ApiResult<List<String>> importHistoryBatch(@RequestParam("file") MultipartFile file,
@RequestParam(value = "companyId", required = false) Integer companyId) {
List<String> errorMessages = new ArrayList<>();
int successCount = 0;
Set<Integer> touchedCompanyIds = new HashSet<>();
try {
int sheetIndex = ExcelImportSupport.findSheetIndex(file, "历史立案信息");
if (sheetIndex < 0) {
return fail("未读取到数据,请确认文件中存在“历史立案信息”选项卡且表头与示例格式一致", null);
}
ExcelImportSupport.ImportResult<CreditCaseFilingImportParam> importResult = ExcelImportSupport.read(
file, CreditCaseFilingImportParam.class, this::isEmptyImportRow, sheetIndex);
List<CreditCaseFilingImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows();
int usedSheetIndex = importResult.getSheetIndex();
if (CollectionUtils.isEmpty(list)) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
User loginUser = getLoginUser();
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditCaseFiling> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditCaseFilingImportParam param = list.get(i);
int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows;
try {
CreditCaseFiling item = convertImportParamToEntity(param);
if (item.getCaseNumber() != null) {
item.setCaseNumber(item.getCaseNumber().trim());
}
if (ImportHelper.isBlank(item.getCaseNumber())) {
errorMessages.add("" + excelRowNumber + "行:案号不能为空");
continue;
}
String link = urlByCaseNumber.get(item.getCaseNumber());
if (!ImportHelper.isBlank(link)) {
item.setUrl(link.trim());
}
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
}
if (item.getTenantId() == null && currentTenantId != null) {
item.setTenantId(currentTenantId);
}
if (item.getStatus() == null) {
item.setStatus(0);
}
if (item.getDeleted() == null) {
item.setDeleted(0);
}
if (item.getRecommend() == null) {
item.setRecommend(0);
}
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCaseFilingService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditCaseFiling::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCaseFilingService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditCaseFiling::getCaseNumber,
"",
errorMessages
);
}
creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.CASE_FILING, touchedCompanyIds);
if (errorMessages.isEmpty()) {
return success("成功导入" + successCount + "条数据", null);
}
return success("导入完成,成功" + successCount + "条,失败" + errorMessages.size() + "", errorMessages);
} catch (Exception e) {
e.printStackTrace();
return fail("导入失败:" + e.getMessage(), null);
}
}
/**
* 下载立案信息导入模板
*/

View File

@@ -99,7 +99,7 @@ public class CreditCompanyController extends BaseController {
@Operation(summary = "删除企业")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCompanyService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditCompany.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -132,7 +132,7 @@ public class CreditCompanyController extends BaseController {
@Operation(summary = "批量删除企业")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCompanyService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditCompany.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -149,6 +149,7 @@ public class CreditCompanyController extends BaseController {
List<String> errorMessages = new ArrayList<>();
int insertedCount = 0;
Set<String> touchedMatchNames = new HashSet<>();
String refreshWarning = null;
try {
List<CreditCompanyImportParam> list = null;
@@ -324,13 +325,31 @@ public class CreditCompanyController extends BaseController {
}
}
}
creditCompanyRecordCountService.refreshAll(touchedCompanyIds);
try {
creditCompanyRecordCountService.refreshAll(touchedCompanyIds);
} catch (Exception ex) {
// 导入本身已经成功写入,回填计数字段失败不应导致整个导入失败(可后续单独重试刷新)。
String msg = ex.getMessage();
if (msg != null && msg.length() > 300) {
msg = msg.substring(0, 300) + "...";
}
refreshWarning = "关联记录数回填失败:" + (msg != null ? msg : ex.getClass().getSimpleName());
ex.printStackTrace();
}
}
if (errorMessages.isEmpty()) {
return success("成功入库" + insertedCount + "条数据", null);
String msg = "成功入库" + insertedCount + "条数据";
if (refreshWarning != null) {
msg = msg + "" + refreshWarning;
}
return success(msg, null);
} else {
return success("导入完成,入库" + insertedCount + "条,失败" + errorMessages.size() + "", errorMessages);
String msg = "导入完成,入库" + insertedCount + "条,失败" + errorMessages.size() + "";
if (refreshWarning != null) {
msg = msg + "" + refreshWarning;
}
return success(msg, errorMessages);
}
} catch (Exception e) {

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditCompetitor;
import com.gxwebsoft.credit.param.CreditCompetitorImportParam;
import com.gxwebsoft.credit.param.CreditCompetitorParam;
@@ -104,7 +105,7 @@ public class CreditCompetitorController extends BaseController {
@Operation(summary = "删除竞争对手")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCompetitorService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditCompetitor.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditCompetitorController extends BaseController {
@Operation(summary = "批量删除竞争对手")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCompetitorService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditCompetitor.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditCompetitorController extends BaseController {
CreditCompetitor::getName,
CreditCompetitor::getCompanyId,
CreditCompetitor::setCompanyId,
CreditCompetitor::setCompanyName,
CreditCompetitor::getHasData,
CreditCompetitor::setHasData,
CreditCompetitor::getTenantId,
@@ -212,6 +214,11 @@ public class CreditCompetitorController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = ExcelImportSupport.readUrlByKey(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "企业名称");
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -222,7 +229,7 @@ public class CreditCompetitorController extends BaseController {
CreditCompetitorImportParam param = list.get(i);
try {
CreditCompetitor item = convertImportParamToEntity(param);
// name 才是持久化字段companyName 为关联查询的临时字段exist=false导入时不应使用
// name 为竞争对手企业名称companyName 为主体企业名称
if (!ImportHelper.isBlank(item.getName())) {
String link = urlByName.get(item.getName().trim());
if (!ImportHelper.isBlank(link)) {
@@ -232,6 +239,9 @@ public class CreditCompetitorController extends BaseController {
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
@@ -262,38 +272,13 @@ public class CreditCompetitorController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCompetitorService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditCompetitorService,
chunkItems,
CreditCompetitor::getId,
CreditCompetitor::setId,
CreditCompetitor::getName,
CreditCompetitor::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditCompetitorService.save(rowItem);
if (!saved) {
CreditCompetitor existing = creditCompetitorService.lambdaQuery()
.eq(CreditCompetitor::getName, rowItem.getName())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditCompetitorService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditCompetitor::getName,
"",
errorMessages
);
chunkItems.clear();
@@ -307,38 +292,13 @@ public class CreditCompetitorController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCompetitorService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditCompetitorService,
chunkItems,
CreditCompetitor::getId,
CreditCompetitor::setId,
CreditCompetitor::getName,
CreditCompetitor::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditCompetitorService.save(rowItem);
if (!saved) {
CreditCompetitor existing = creditCompetitorService.lambdaQuery()
.eq(CreditCompetitor::getName, rowItem.getName())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditCompetitorService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditCompetitor::getName,
"",
errorMessages
);
}

View File

@@ -104,7 +104,7 @@ public class CreditCourtAnnouncementController extends BaseController {
@Operation(summary = "删除法院公告司法大数据")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCourtAnnouncementService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditCourtAnnouncement.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +137,7 @@ public class CreditCourtAnnouncementController extends BaseController {
@Operation(summary = "批量删除法院公告司法大数据")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCourtAnnouncementService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditCourtAnnouncement.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -265,38 +265,13 @@ public class CreditCourtAnnouncementController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtAnnouncementService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditCourtAnnouncementService,
chunkItems,
CreditCourtAnnouncement::getId,
CreditCourtAnnouncement::setId,
CreditCourtAnnouncement::getCaseNumber,
CreditCourtAnnouncement::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditCourtAnnouncementService.save(rowItem);
if (!saved) {
CreditCourtAnnouncement existing = creditCourtAnnouncementService.lambdaQuery()
.eq(CreditCourtAnnouncement::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditCourtAnnouncementService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditCourtAnnouncement::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
@@ -310,38 +285,13 @@ public class CreditCourtAnnouncementController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtAnnouncementService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditCourtAnnouncementService,
chunkItems,
CreditCourtAnnouncement::getId,
CreditCourtAnnouncement::setId,
CreditCourtAnnouncement::getCaseNumber,
CreditCourtAnnouncement::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditCourtAnnouncementService.save(rowItem);
if (!saved) {
CreditCourtAnnouncement existing = creditCourtAnnouncementService.lambdaQuery()
.eq(CreditCourtAnnouncement::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditCourtAnnouncementService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditCourtAnnouncement::getCaseNumber,
"",
errorMessages
);
}
@@ -359,6 +309,137 @@ public class CreditCourtAnnouncementController extends BaseController {
}
}
/**
* 批量导入历史法院公告(仅解析“历史法院公告”选项卡;兼容“历史法庭公告”)
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditCourtAnnouncement:save')")
@Operation(summary = "批量导入历史法院公告司法大数据")
@PostMapping("/import/history")
public ApiResult<List<String>> importHistoryBatch(@RequestParam("file") MultipartFile file,
@RequestParam(value = "companyId", required = false) Integer companyId) {
List<String> errorMessages = new ArrayList<>();
int successCount = 0;
Set<Integer> touchedCompanyIds = new HashSet<>();
try {
int sheetIndex = ExcelImportSupport.findSheetIndex(file, "历史法院公告");
if (sheetIndex < 0) {
// 兼容旧命名
sheetIndex = ExcelImportSupport.findSheetIndex(file, "历史法庭公告");
}
if (sheetIndex < 0) {
return fail("未读取到数据,请确认文件中存在“历史法院公告”选项卡且表头与示例格式一致", null);
}
ExcelImportSupport.ImportResult<CreditCourtAnnouncementImportParam> importResult = ExcelImportSupport.read(
file, CreditCourtAnnouncementImportParam.class, this::isEmptyImportRow, sheetIndex);
List<CreditCourtAnnouncementImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows();
int usedSheetIndex = importResult.getSheetIndex();
if (CollectionUtils.isEmpty(list)) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
User loginUser = getLoginUser();
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditCourtAnnouncement> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditCourtAnnouncementImportParam param = list.get(i);
int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows;
try {
CreditCourtAnnouncement item = convertImportParamToEntity(param);
if (item.getCaseNumber() != null) {
item.setCaseNumber(item.getCaseNumber().trim());
}
if (ImportHelper.isBlank(item.getCaseNumber())) {
errorMessages.add("" + excelRowNumber + "行:案号不能为空");
continue;
}
String link = urlByCaseNumber.get(item.getCaseNumber());
if (!ImportHelper.isBlank(link)) {
item.setUrl(link.trim());
}
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
}
if (item.getTenantId() == null && currentTenantId != null) {
item.setTenantId(currentTenantId);
}
if (item.getStatus() == null) {
item.setStatus(0);
}
if (item.getDeleted() == null) {
item.setDeleted(0);
}
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
if (item.getRecommend() == null) {
item.setRecommend(0);
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtAnnouncementService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditCourtAnnouncement::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtAnnouncementService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditCourtAnnouncement::getCaseNumber,
"",
errorMessages
);
}
creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.COURT_ANNOUNCEMENT, touchedCompanyIds);
if (errorMessages.isEmpty()) {
return success("成功导入" + successCount + "条数据", null);
}
return success("导入完成,成功" + successCount + "条,失败" + errorMessages.size() + "", errorMessages);
} catch (Exception e) {
e.printStackTrace();
return fail("导入失败:" + e.getMessage(), null);
}
}
/**
* 下载法院公告司法大数据导入模板
*/

View File

@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditCourtSessionController extends BaseController {
@Operation(summary = "删除开庭公告司法大数据")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCourtSessionService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditCourtSession.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditCourtSessionController extends BaseController {
@Operation(summary = "批量删除开庭公告司法大数据")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCourtSessionService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditCourtSession.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -269,38 +268,13 @@ public class CreditCourtSessionController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtSessionService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditCourtSessionService,
chunkItems,
CreditCourtSession::getId,
CreditCourtSession::setId,
CreditCourtSession::getCaseNumber,
CreditCourtSession::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditCourtSessionService.save(rowItem);
if (!saved) {
CreditCourtSession existing = creditCourtSessionService.lambdaQuery()
.eq(CreditCourtSession::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditCourtSessionService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditCourtSession::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
@@ -314,38 +288,13 @@ public class CreditCourtSessionController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtSessionService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditCourtSessionService,
chunkItems,
CreditCourtSession::getId,
CreditCourtSession::setId,
CreditCourtSession::getCaseNumber,
CreditCourtSession::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditCourtSessionService.save(rowItem);
if (!saved) {
CreditCourtSession existing = creditCourtSessionService.lambdaQuery()
.eq(CreditCourtSession::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditCourtSessionService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditCourtSession::getCaseNumber,
"",
errorMessages
);
}
@@ -365,7 +314,7 @@ public class CreditCourtSessionController extends BaseController {
/**
* 批量导入历史开庭公告(仅解析“历史开庭公告”选项卡)
* 规则:案号相同则覆盖更新recommend++ 记录更新次数);案号不存在则插入。
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditCourtSession:save')")
@Operation(summary = "批量导入历史开庭公告司法大数据")
@@ -398,8 +347,10 @@ public class CreditCourtSessionController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
LinkedHashMap<String, CreditCourtSession> latestByCaseNumber = new LinkedHashMap<>();
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>();
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditCourtSession> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditCourtSessionImportParam param = list.get(i);
@@ -437,121 +388,42 @@ public class CreditCourtSessionController extends BaseController {
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item);
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber);
if (item.getRecommend() == null) {
item.setRecommend(0);
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtSessionService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditCourtSession::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (latestByCaseNumber.isEmpty()) {
if (errorMessages.isEmpty()) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
return success("导入完成成功0条失败" + errorMessages.size() + "", errorMessages);
}
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditCourtSession> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (Map.Entry<String, CreditCourtSession> entry : latestByCaseNumber.entrySet()) {
String caseNumber = entry.getKey();
CreditCourtSession item = entry.getValue();
Integer rowNo = latestRowByCaseNumber.get(caseNumber);
chunkItems.add(item);
chunkRowNumbers.add(rowNo != null ? rowNo : -1);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditCourtSessionService,
chunkItems,
CreditCourtSession::getId,
CreditCourtSession::setId,
CreditCourtSession::getCaseNumber,
CreditCourtSession::getCaseNumber,
CreditCourtSession::getRecommend,
CreditCourtSession::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditCourtSessionService.save(rowItem);
if (!saved) {
CreditCourtSession existing = creditCourtSessionService.lambdaQuery()
.eq(CreditCourtSession::getCaseNumber, rowItem.getCaseNumber())
.select(CreditCourtSession::getId, CreditCourtSession::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditCourtSessionService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtSessionService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditCourtSessionService,
chunkItems,
CreditCourtSession::getId,
CreditCourtSession::setId,
CreditCourtSession::getCaseNumber,
CreditCourtSession::getCaseNumber,
CreditCourtSession::getRecommend,
CreditCourtSession::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditCourtSessionService.save(rowItem);
if (!saved) {
CreditCourtSession existing = creditCourtSessionService.lambdaQuery()
.eq(CreditCourtSession::getCaseNumber, rowItem.getCaseNumber())
.select(CreditCourtSession::getId, CreditCourtSession::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditCourtSessionService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditCourtSession::getCaseNumber,
"",
errorMessages
);
}

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditCustomer;
import com.gxwebsoft.credit.param.CreditCustomerImportParam;
import com.gxwebsoft.credit.param.CreditCustomerParam;
@@ -24,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -100,7 +102,7 @@ public class CreditCustomerController extends BaseController {
@Operation(summary = "删除客户")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCustomerService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditCustomer.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -133,7 +135,7 @@ public class CreditCustomerController extends BaseController {
@Operation(summary = "批量删除客户")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCustomerService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditCustomer.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -166,6 +168,7 @@ public class CreditCustomerController extends BaseController {
CreditCustomer::getName,
CreditCustomer::getCompanyId,
CreditCustomer::setCompanyId,
CreditCustomer::setCompanyName,
CreditCustomer::getHasData,
CreditCustomer::setHasData,
CreditCustomer::getTenantId,
@@ -207,6 +210,11 @@ public class CreditCustomerController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "客户");
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -219,7 +227,8 @@ public class CreditCustomerController extends BaseController {
try {
CreditCustomer item = convertImportParamToEntity(param);
if (!ImportHelper.isBlank(item.getName())) {
String link = urlByName.get(item.getName().trim());
item.setName(item.getName().trim());
String link = urlByName.get(item.getName());
if (!ImportHelper.isBlank(link)) {
item.setUrl(link.trim());
}
@@ -227,6 +236,9 @@ public class CreditCustomerController extends BaseController {
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
@@ -256,116 +268,7 @@ public class CreditCustomerController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> {
// 批内一次查库,避免逐行查/写导致数据库压力过大
List<String> names = new ArrayList<>(chunkItems.size());
for (CreditCustomer it : chunkItems) {
if (it != null && !ImportHelper.isBlank(it.getName())) {
names.add(it.getName().trim());
}
}
List<CreditCustomer> existingList = names.isEmpty()
? new ArrayList<>()
: creditCustomerService.lambdaQuery()
.in(CreditCustomer::getName, names)
.list();
java.util.Map<String, CreditCustomer> existingByName = new java.util.HashMap<>();
for (CreditCustomer existing : existingList) {
if (existing != null && !ImportHelper.isBlank(existing.getName())) {
existingByName.putIfAbsent(existing.getName().trim(), existing);
}
}
java.util.Map<String, CreditCustomer> latestByName = new java.util.HashMap<>();
int acceptedRows = 0;
for (int idx = 0; idx < chunkItems.size(); idx++) {
CreditCustomer it = chunkItems.get(idx);
int rowNo = (idx < chunkRowNumbers.size()) ? chunkRowNumbers.get(idx) : -1;
if (it == null || ImportHelper.isBlank(it.getName())) {
continue;
}
String name = it.getName().trim();
CreditCustomer existing = existingByName.get(name);
if (existing != null) {
Integer existingTenantId = existing.getTenantId();
if (it.getTenantId() != null
&& existingTenantId != null
&& !it.getTenantId().equals(existingTenantId)) {
errorMessages.add("" + rowNo + "行:客户名称已存在且归属其他租户,无法导入");
continue;
}
it.setId(existing.getId());
if (existingTenantId != null) {
it.setTenantId(existingTenantId);
}
}
// 同名多行:保留最后一行的值(等价于“先插入/更新,再被后续行更新”)
latestByName.put(name, it);
acceptedRows++;
}
List<CreditCustomer> updates = new ArrayList<>();
List<CreditCustomer> inserts = new ArrayList<>();
for (CreditCustomer it : latestByName.values()) {
if (it.getId() != null) {
updates.add(it);
} else {
inserts.add(it);
}
}
if (!updates.isEmpty()) {
creditCustomerService.updateBatchById(updates, mpBatchSize);
}
if (!inserts.isEmpty()) {
creditCustomerService.saveBatch(inserts, mpBatchSize);
}
return acceptedRows;
},
(rowItem, rowNumber) -> {
CreditCustomer existing = creditCustomerService.lambdaQuery()
.eq(CreditCustomer::getName, rowItem.getName())
.one();
if (existing != null) {
Integer existingTenantId = existing.getTenantId();
if (rowItem.getTenantId() != null
&& existingTenantId != null
&& !rowItem.getTenantId().equals(existingTenantId)) {
errorMessages.add("" + rowNumber + "行:客户名称已存在且归属其他租户,无法导入");
return false;
}
rowItem.setId(existing.getId());
if (existingTenantId != null) {
rowItem.setTenantId(existingTenantId);
}
return creditCustomerService.updateById(rowItem);
}
try {
return creditCustomerService.save(rowItem);
} catch (DataIntegrityViolationException e) {
if (!isDuplicateCustomerName(e)) {
throw e;
}
CreditCustomer dbExisting = creditCustomerService.lambdaQuery()
.eq(CreditCustomer::getName, rowItem.getName())
.one();
if (dbExisting != null) {
Integer existingTenantId = dbExisting.getTenantId();
rowItem.setId(dbExisting.getId());
if (existingTenantId != null) {
rowItem.setTenantId(existingTenantId);
}
return creditCustomerService.updateById(rowItem);
}
}
errorMessages.add("" + rowNumber + "行:保存失败");
return false;
},
errorMessages
);
successCount += persistImportChunk(chunkItems, chunkRowNumbers, mpBatchSize, errorMessages);
chunkItems.clear();
chunkRowNumbers.clear();
}
@@ -376,114 +279,7 @@ public class CreditCustomerController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> {
List<String> names = new ArrayList<>(chunkItems.size());
for (CreditCustomer it : chunkItems) {
if (it != null && !ImportHelper.isBlank(it.getName())) {
names.add(it.getName().trim());
}
}
List<CreditCustomer> existingList = names.isEmpty()
? new ArrayList<>()
: creditCustomerService.lambdaQuery()
.in(CreditCustomer::getName, names)
.list();
java.util.Map<String, CreditCustomer> existingByName = new java.util.HashMap<>();
for (CreditCustomer existing : existingList) {
if (existing != null && !ImportHelper.isBlank(existing.getName())) {
existingByName.putIfAbsent(existing.getName().trim(), existing);
}
}
java.util.Map<String, CreditCustomer> latestByName = new java.util.HashMap<>();
int acceptedRows = 0;
for (int idx = 0; idx < chunkItems.size(); idx++) {
CreditCustomer it = chunkItems.get(idx);
int rowNo = (idx < chunkRowNumbers.size()) ? chunkRowNumbers.get(idx) : -1;
if (it == null || ImportHelper.isBlank(it.getName())) {
continue;
}
String name = it.getName().trim();
CreditCustomer existing = existingByName.get(name);
if (existing != null) {
Integer existingTenantId = existing.getTenantId();
if (it.getTenantId() != null
&& existingTenantId != null
&& !it.getTenantId().equals(existingTenantId)) {
errorMessages.add("" + rowNo + "行:客户名称已存在且归属其他租户,无法导入");
continue;
}
it.setId(existing.getId());
if (existingTenantId != null) {
it.setTenantId(existingTenantId);
}
}
latestByName.put(name, it);
acceptedRows++;
}
List<CreditCustomer> updates = new ArrayList<>();
List<CreditCustomer> inserts = new ArrayList<>();
for (CreditCustomer it : latestByName.values()) {
if (it.getId() != null) {
updates.add(it);
} else {
inserts.add(it);
}
}
if (!updates.isEmpty()) {
creditCustomerService.updateBatchById(updates, mpBatchSize);
}
if (!inserts.isEmpty()) {
creditCustomerService.saveBatch(inserts, mpBatchSize);
}
return acceptedRows;
},
(rowItem, rowNumber) -> {
CreditCustomer existing = creditCustomerService.lambdaQuery()
.eq(CreditCustomer::getName, rowItem.getName())
.one();
if (existing != null) {
Integer existingTenantId = existing.getTenantId();
if (rowItem.getTenantId() != null
&& existingTenantId != null
&& !rowItem.getTenantId().equals(existingTenantId)) {
errorMessages.add("" + rowNumber + "行:客户名称已存在且归属其他租户,无法导入");
return false;
}
rowItem.setId(existing.getId());
if (existingTenantId != null) {
rowItem.setTenantId(existingTenantId);
}
return creditCustomerService.updateById(rowItem);
}
try {
return creditCustomerService.save(rowItem);
} catch (DataIntegrityViolationException e) {
if (!isDuplicateCustomerName(e)) {
throw e;
}
CreditCustomer dbExisting = creditCustomerService.lambdaQuery()
.eq(CreditCustomer::getName, rowItem.getName())
.one();
if (dbExisting != null) {
Integer existingTenantId = dbExisting.getTenantId();
rowItem.setId(dbExisting.getId());
if (existingTenantId != null) {
rowItem.setTenantId(existingTenantId);
}
return creditCustomerService.updateById(rowItem);
}
}
errorMessages.add("" + rowNumber + "行:保存失败");
return false;
},
errorMessages
);
successCount += persistImportChunk(chunkItems, chunkRowNumbers, mpBatchSize, errorMessages);
}
creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.CUSTOMER, touchedCompanyIds);
@@ -554,19 +350,71 @@ public class CreditCustomerController extends BaseController {
return value.trim();
}
private boolean isDuplicateCustomerName(DataIntegrityViolationException e) {
private int persistImportChunk(List<CreditCustomer> items,
List<Integer> excelRowNumbers,
int mpBatchSize,
List<String> errorMessages) {
return batchImportSupport.persistChunkWithFallback(
items,
excelRowNumbers,
() -> {
boolean ok = creditCustomerService.saveBatch(items, mpBatchSize);
if (!ok) {
throw new RuntimeException("批量保存失败");
}
return items.size();
},
(rowItem, rowNumber) -> {
try {
boolean saved = creditCustomerService.save(rowItem);
if (saved) {
return true;
}
if (rowNumber != null && rowNumber > 0) {
errorMessages.add("" + rowNumber + "行:保存失败");
} else {
errorMessages.add("保存失败");
}
return false;
} catch (DataIntegrityViolationException e) {
if (isDuplicateKey(e)) {
String name = rowItem != null ? rowItem.getName() : null;
String label = ImportHelper.isBlank(name) ? "数据" : ("客户【" + name.trim() + "");
if (rowNumber != null && rowNumber > 0) {
errorMessages.add("" + rowNumber + "行:" + label + "重复(唯一索引冲突)");
} else {
errorMessages.add(label + "重复(唯一索引冲突)");
}
return false;
}
throw e;
}
},
errorMessages
);
}
private static boolean isDuplicateKey(DataIntegrityViolationException e) {
// Prefer structured detection (SQLState / vendor error code), fall back to message contains.
for (Throwable t = e; t != null; t = t.getCause()) {
if (t instanceof SQLException) {
SQLException se = (SQLException) t;
// MySQL: 1062 Duplicate entry; PostgreSQL/H2: SQLState 23505 unique_violation
if (se.getErrorCode() == 1062) {
return true;
}
if ("23505".equals(se.getSQLState())) {
return true;
}
}
}
Throwable mostSpecificCause = e.getMostSpecificCause();
String message = mostSpecificCause != null ? mostSpecificCause.getMessage() : e.getMessage();
if (message == null) {
return false;
}
String lower = message.toLowerCase();
if (!lower.contains("duplicate")) {
return false;
}
return lower.contains("credit_customer.name")
|| lower.contains("for key 'name'")
|| lower.contains("for key `name`");
return lower.contains("duplicate") && lower.contains("key");
}
}

View File

@@ -104,7 +104,7 @@ public class CreditDeliveryNoticeController extends BaseController {
@Operation(summary = "删除送达公告司法大数据")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditDeliveryNoticeService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditDeliveryNotice.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +137,7 @@ public class CreditDeliveryNoticeController extends BaseController {
@Operation(summary = "批量删除送达公告司法大数据")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditDeliveryNoticeService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditDeliveryNotice.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -266,38 +266,13 @@ public class CreditDeliveryNoticeController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditDeliveryNoticeService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditDeliveryNoticeService,
chunkItems,
CreditDeliveryNotice::getId,
CreditDeliveryNotice::setId,
CreditDeliveryNotice::getCaseNumber,
CreditDeliveryNotice::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditDeliveryNoticeService.save(rowItem);
if (!saved) {
CreditDeliveryNotice existing = creditDeliveryNoticeService.lambdaQuery()
.eq(CreditDeliveryNotice::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditDeliveryNoticeService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditDeliveryNotice::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
@@ -311,38 +286,13 @@ public class CreditDeliveryNoticeController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditDeliveryNoticeService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditDeliveryNoticeService,
chunkItems,
CreditDeliveryNotice::getId,
CreditDeliveryNotice::setId,
CreditDeliveryNotice::getCaseNumber,
CreditDeliveryNotice::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditDeliveryNoticeService.save(rowItem);
if (!saved) {
CreditDeliveryNotice existing = creditDeliveryNoticeService.lambdaQuery()
.eq(CreditDeliveryNotice::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditDeliveryNoticeService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditDeliveryNotice::getCaseNumber,
"",
errorMessages
);
}
@@ -360,6 +310,134 @@ public class CreditDeliveryNoticeController extends BaseController {
}
}
/**
* 批量导入历史送达公告(仅解析“历史送达公告”选项卡)
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditDeliveryNotice:save')")
@Operation(summary = "批量导入历史送达公告司法大数据")
@PostMapping("/import/history")
public ApiResult<List<String>> importHistoryBatch(@RequestParam("file") MultipartFile file,
@RequestParam(value = "companyId", required = false) Integer companyId) {
List<String> errorMessages = new ArrayList<>();
int successCount = 0;
Set<Integer> touchedCompanyIds = new HashSet<>();
try {
int sheetIndex = ExcelImportSupport.findSheetIndex(file, "历史送达公告");
if (sheetIndex < 0) {
return fail("未读取到数据,请确认文件中存在“历史送达公告”选项卡且表头与示例格式一致", null);
}
ExcelImportSupport.ImportResult<CreditDeliveryNoticeImportParam> importResult = ExcelImportSupport.read(
file, CreditDeliveryNoticeImportParam.class, this::isEmptyImportRow, sheetIndex);
List<CreditDeliveryNoticeImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows();
int usedSheetIndex = importResult.getSheetIndex();
if (CollectionUtils.isEmpty(list)) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
User loginUser = getLoginUser();
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditDeliveryNotice> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditDeliveryNoticeImportParam param = list.get(i);
int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows;
try {
CreditDeliveryNotice item = convertImportParamToEntity(param);
if (item.getCaseNumber() != null) {
item.setCaseNumber(item.getCaseNumber().trim());
}
if (ImportHelper.isBlank(item.getCaseNumber())) {
errorMessages.add("" + excelRowNumber + "行:案号不能为空");
continue;
}
String link = urlByCaseNumber.get(item.getCaseNumber());
if (!ImportHelper.isBlank(link)) {
item.setUrl(link.trim());
}
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
}
if (item.getTenantId() == null && currentTenantId != null) {
item.setTenantId(currentTenantId);
}
if (item.getStatus() == null) {
item.setStatus(0);
}
if (item.getDeleted() == null) {
item.setDeleted(0);
}
if (item.getRecommend() == null) {
item.setRecommend(0);
}
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditDeliveryNoticeService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditDeliveryNotice::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditDeliveryNoticeService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditDeliveryNotice::getCaseNumber,
"",
errorMessages
);
}
creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.DELIVERY_NOTICE, touchedCompanyIds);
if (errorMessages.isEmpty()) {
return success("成功导入" + successCount + "条数据", null);
}
return success("导入完成,成功" + successCount + "条,失败" + errorMessages.size() + "", errorMessages);
} catch (Exception e) {
e.printStackTrace();
return fail("导入失败:" + e.getMessage(), null);
}
}
/**
* 下载送达公告导入模板
*/

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditExternal;
import com.gxwebsoft.credit.param.CreditExternalImportParam;
import com.gxwebsoft.credit.param.CreditExternalParam;
@@ -104,7 +105,7 @@ public class CreditExternalController extends BaseController {
@Operation(summary = "删除对外投资")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditExternalService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditExternal.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditExternalController extends BaseController {
@Operation(summary = "批量删除对外投资")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditExternalService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditExternal.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditExternalController extends BaseController {
CreditExternal::getName,
CreditExternal::getCompanyId,
CreditExternal::setCompanyId,
CreditExternal::setCompanyName,
CreditExternal::getHasData,
CreditExternal::setHasData,
CreditExternal::getTenantId,
@@ -211,6 +213,11 @@ public class CreditExternalController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "被投资企业名称");
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -230,6 +237,9 @@ public class CreditExternalController extends BaseController {
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
@@ -260,38 +270,13 @@ public class CreditExternalController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditExternalService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditExternalService,
chunkItems,
CreditExternal::getId,
CreditExternal::setId,
CreditExternal::getName,
CreditExternal::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditExternalService.save(rowItem);
if (!saved) {
CreditExternal existing = creditExternalService.lambdaQuery()
.eq(CreditExternal::getName, rowItem.getName())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditExternalService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditExternal::getName,
"",
errorMessages
);
chunkItems.clear();
@@ -305,38 +290,13 @@ public class CreditExternalController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditExternalService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditExternalService,
chunkItems,
CreditExternal::getId,
CreditExternal::setId,
CreditExternal::getName,
CreditExternal::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditExternalService.save(rowItem);
if (!saved) {
CreditExternal existing = creditExternalService.lambdaQuery()
.eq(CreditExternal::getName, rowItem.getName())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditExternalService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditExternal::getName,
"",
errorMessages
);
}

View File

@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditFinalVersionController extends BaseController {
@Operation(summary = "删除终本案件")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditFinalVersionService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditFinalVersion.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditFinalVersionController extends BaseController {
@Operation(summary = "批量删除终本案件")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditFinalVersionService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditFinalVersion.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -269,38 +268,13 @@ public class CreditFinalVersionController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditFinalVersionService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditFinalVersionService,
chunkItems,
CreditFinalVersion::getId,
CreditFinalVersion::setId,
CreditFinalVersion::getCaseNumber,
CreditFinalVersion::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditFinalVersionService.save(rowItem);
if (!saved) {
CreditFinalVersion existing = creditFinalVersionService.lambdaQuery()
.eq(CreditFinalVersion::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditFinalVersionService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditFinalVersion::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
@@ -314,38 +288,13 @@ public class CreditFinalVersionController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditFinalVersionService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditFinalVersionService,
chunkItems,
CreditFinalVersion::getId,
CreditFinalVersion::setId,
CreditFinalVersion::getCaseNumber,
CreditFinalVersion::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditFinalVersionService.save(rowItem);
if (!saved) {
CreditFinalVersion existing = creditFinalVersionService.lambdaQuery()
.eq(CreditFinalVersion::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditFinalVersionService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditFinalVersion::getCaseNumber,
"",
errorMessages
);
}
@@ -365,7 +314,7 @@ public class CreditFinalVersionController extends BaseController {
/**
* 批量导入历史终本案件(仅解析“历史终本案件”选项卡)
* 规则:案号相同则覆盖更新recommend++ 记录更新次数);案号不存在则插入。
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditFinalVersion:save')")
@Operation(summary = "批量导入历史终本案件")
@@ -398,8 +347,10 @@ public class CreditFinalVersionController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
LinkedHashMap<String, CreditFinalVersion> latestByCaseNumber = new LinkedHashMap<>();
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>();
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditFinalVersion> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditFinalVersionImportParam param = list.get(i);
@@ -437,121 +388,42 @@ public class CreditFinalVersionController extends BaseController {
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item);
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber);
if (item.getRecommend() == null) {
item.setRecommend(0);
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditFinalVersionService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditFinalVersion::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (latestByCaseNumber.isEmpty()) {
if (errorMessages.isEmpty()) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
return success("导入完成成功0条失败" + errorMessages.size() + "", errorMessages);
}
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditFinalVersion> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (Map.Entry<String, CreditFinalVersion> entry : latestByCaseNumber.entrySet()) {
String caseNumber = entry.getKey();
CreditFinalVersion item = entry.getValue();
Integer rowNo = latestRowByCaseNumber.get(caseNumber);
chunkItems.add(item);
chunkRowNumbers.add(rowNo != null ? rowNo : -1);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditFinalVersionService,
chunkItems,
CreditFinalVersion::getId,
CreditFinalVersion::setId,
CreditFinalVersion::getCaseNumber,
CreditFinalVersion::getCaseNumber,
CreditFinalVersion::getRecommend,
CreditFinalVersion::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditFinalVersionService.save(rowItem);
if (!saved) {
CreditFinalVersion existing = creditFinalVersionService.lambdaQuery()
.eq(CreditFinalVersion::getCaseNumber, rowItem.getCaseNumber())
.select(CreditFinalVersion::getId, CreditFinalVersion::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditFinalVersionService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditFinalVersionService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditFinalVersionService,
chunkItems,
CreditFinalVersion::getId,
CreditFinalVersion::setId,
CreditFinalVersion::getCaseNumber,
CreditFinalVersion::getCaseNumber,
CreditFinalVersion::getRecommend,
CreditFinalVersion::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditFinalVersionService.save(rowItem);
if (!saved) {
CreditFinalVersion existing = creditFinalVersionService.lambdaQuery()
.eq(CreditFinalVersion::getCaseNumber, rowItem.getCaseNumber())
.select(CreditFinalVersion::getId, CreditFinalVersion::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditFinalVersionService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditFinalVersion::getCaseNumber,
"",
errorMessages
);
}

View File

@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditGqdjController extends BaseController {
@Operation(summary = "删除股权冻结")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditGqdjService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditGqdj.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditGqdjController extends BaseController {
@Operation(summary = "批量删除股权冻结")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditGqdjService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditGqdj.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -356,38 +355,13 @@ public class CreditGqdjController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditGqdjService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditGqdjService,
chunkItems,
CreditGqdj::getId,
CreditGqdj::setId,
CreditGqdj::getCaseNumber,
CreditGqdj::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditGqdjService.save(rowItem);
if (!saved) {
CreditGqdj existing = creditGqdjService.lambdaQuery()
.eq(CreditGqdj::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditGqdjService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditGqdj::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
@@ -401,38 +375,13 @@ public class CreditGqdjController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditGqdjService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditGqdjService,
chunkItems,
CreditGqdj::getId,
CreditGqdj::setId,
CreditGqdj::getCaseNumber,
CreditGqdj::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditGqdjService.save(rowItem);
if (!saved) {
CreditGqdj existing = creditGqdjService.lambdaQuery()
.eq(CreditGqdj::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditGqdjService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditGqdj::getCaseNumber,
"",
errorMessages
);
}
@@ -452,7 +401,7 @@ public class CreditGqdjController extends BaseController {
/**
* 批量导入历史股权冻结(仅解析“历史股权冻结”选项卡)
* 规则:执行通知文书号/案号相同则覆盖更新recommend++ 记录更新次数);不存在则插入。
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditGqdj:save')")
@Operation(summary = "批量导入历史股权冻结司法大数据")
@@ -547,8 +496,10 @@ public class CreditGqdjController extends BaseController {
}
}
LinkedHashMap<String, CreditGqdj> latestByCaseNumber = new LinkedHashMap<>();
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>();
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditGqdj> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditGqdjImportParam param = list.get(i);
@@ -588,123 +539,44 @@ public class CreditGqdjController extends BaseController {
item.setDeleted(0);
}
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
item.setDataType("失效");
latestByCaseNumber.put(item.getCaseNumber(), item);
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber);
if (item.getRecommend() == null) {
item.setRecommend(0);
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditGqdjService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditGqdj::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (latestByCaseNumber.isEmpty()) {
if (errorMessages.isEmpty()) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
return success("导入完成成功0条失败" + errorMessages.size() + "", errorMessages);
}
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditGqdj> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (Map.Entry<String, CreditGqdj> entry : latestByCaseNumber.entrySet()) {
String caseNumber = entry.getKey();
CreditGqdj item = entry.getValue();
Integer rowNo = latestRowByCaseNumber.get(caseNumber);
chunkItems.add(item);
chunkRowNumbers.add(rowNo != null ? rowNo : -1);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditGqdjService,
chunkItems,
CreditGqdj::getId,
CreditGqdj::setId,
CreditGqdj::getCaseNumber,
CreditGqdj::getCaseNumber,
CreditGqdj::getRecommend,
CreditGqdj::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditGqdjService.save(rowItem);
if (!saved) {
CreditGqdj existing = creditGqdjService.lambdaQuery()
.eq(CreditGqdj::getCaseNumber, rowItem.getCaseNumber())
.select(CreditGqdj::getId, CreditGqdj::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditGqdjService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditGqdjService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditGqdjService,
chunkItems,
CreditGqdj::getId,
CreditGqdj::setId,
CreditGqdj::getCaseNumber,
CreditGqdj::getCaseNumber,
CreditGqdj::getRecommend,
CreditGqdj::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditGqdjService.save(rowItem);
if (!saved) {
CreditGqdj existing = creditGqdjService.lambdaQuery()
.eq(CreditGqdj::getCaseNumber, rowItem.getCaseNumber())
.select(CreditGqdj::getId, CreditGqdj::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditGqdjService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditGqdj::getCaseNumber,
"",
errorMessages
);
}
@@ -795,7 +667,7 @@ public class CreditGqdjController extends BaseController {
} else {
entity.setDataStatus(param.getDataStatus());
}
entity.setDataType("股权冻结");
entity.setDataType(param.getDataType());
entity.setPublicDate(param.getPublicDate());
if (!ImportHelper.isBlank(param.getFreezeDateStart2())) {
entity.setFreezeDateStart(param.getFreezeDateStart2());

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditHistoricalLegalPerson;
import com.gxwebsoft.credit.param.CreditHistoricalLegalPersonImportParam;
import com.gxwebsoft.credit.param.CreditHistoricalLegalPersonParam;
@@ -104,7 +105,7 @@ public class CreditHistoricalLegalPersonController extends BaseController {
@Operation(summary = "删除历史法定代表人")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditHistoricalLegalPersonService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditHistoricalLegalPerson.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditHistoricalLegalPersonController extends BaseController {
@Operation(summary = "批量删除历史法定代表人")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditHistoricalLegalPersonService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditHistoricalLegalPerson.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditHistoricalLegalPersonController extends BaseController {
CreditHistoricalLegalPerson::getName,
CreditHistoricalLegalPerson::getCompanyId,
CreditHistoricalLegalPerson::setCompanyId,
CreditHistoricalLegalPerson::setCompanyName,
CreditHistoricalLegalPerson::getHasData,
CreditHistoricalLegalPerson::setHasData,
CreditHistoricalLegalPerson::getTenantId,
@@ -210,6 +212,11 @@ public class CreditHistoricalLegalPersonController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "名称");
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -229,6 +236,9 @@ public class CreditHistoricalLegalPersonController extends BaseController {
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
@@ -259,86 +269,13 @@ public class CreditHistoricalLegalPersonController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditHistoricalLegalPersonService,
chunkItems,
chunkRowNumbers,
() -> {
// 批内一次查库:按 name in (...) 拉取,再按 registerDate 做内存匹配
List<String> names = new ArrayList<>(chunkItems.size());
for (CreditHistoricalLegalPerson it : chunkItems) {
if (it != null && !ImportHelper.isBlank(it.getName())) {
names.add(it.getName().trim());
}
}
List<CreditHistoricalLegalPerson> existingList = names.isEmpty()
? new ArrayList<>()
: creditHistoricalLegalPersonService.lambdaQuery()
.in(CreditHistoricalLegalPerson::getName, names)
.list();
java.util.Map<String, CreditHistoricalLegalPerson> byName = new java.util.HashMap<>();
java.util.Map<String, CreditHistoricalLegalPerson> byNameDate = new java.util.HashMap<>();
for (CreditHistoricalLegalPerson existing : existingList) {
if (existing == null || ImportHelper.isBlank(existing.getName())) {
continue;
}
String n = existing.getName().trim();
byName.putIfAbsent(n, existing);
String d = ImportHelper.isBlank(existing.getRegisterDate()) ? null : existing.getRegisterDate().trim();
if (d != null) {
byNameDate.putIfAbsent(n + "|" + d, existing);
}
}
List<CreditHistoricalLegalPerson> updates = new ArrayList<>();
List<CreditHistoricalLegalPerson> inserts = new ArrayList<>();
for (CreditHistoricalLegalPerson it : chunkItems) {
if (it == null || ImportHelper.isBlank(it.getName())) {
continue;
}
String n = it.getName().trim();
CreditHistoricalLegalPerson existing;
if (!ImportHelper.isBlank(it.getRegisterDate())) {
String d = it.getRegisterDate().trim();
existing = byNameDate.get(n + "|" + d);
} else {
existing = byName.get(n);
}
if (existing != null) {
it.setId(existing.getId());
updates.add(it);
} else {
inserts.add(it);
}
}
if (!updates.isEmpty()) {
creditHistoricalLegalPersonService.updateBatchById(updates, mpBatchSize);
}
if (!inserts.isEmpty()) {
creditHistoricalLegalPersonService.saveBatch(inserts, mpBatchSize);
}
return updates.size() + inserts.size();
},
(rowItem, rowNumber) -> {
boolean saved = creditHistoricalLegalPersonService.save(rowItem);
if (!saved) {
CreditHistoricalLegalPerson existing = creditHistoricalLegalPersonService.lambdaQuery()
.eq(CreditHistoricalLegalPerson::getName, rowItem.getName())
.eq(!ImportHelper.isBlank(rowItem.getRegisterDate()), CreditHistoricalLegalPerson::getRegisterDate, rowItem.getRegisterDate())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditHistoricalLegalPersonService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditHistoricalLegalPerson::getName,
"",
errorMessages
);
chunkItems.clear();
@@ -352,85 +289,13 @@ public class CreditHistoricalLegalPersonController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditHistoricalLegalPersonService,
chunkItems,
chunkRowNumbers,
() -> {
List<String> names = new ArrayList<>(chunkItems.size());
for (CreditHistoricalLegalPerson it : chunkItems) {
if (it != null && !ImportHelper.isBlank(it.getName())) {
names.add(it.getName().trim());
}
}
List<CreditHistoricalLegalPerson> existingList = names.isEmpty()
? new ArrayList<>()
: creditHistoricalLegalPersonService.lambdaQuery()
.in(CreditHistoricalLegalPerson::getName, names)
.list();
java.util.Map<String, CreditHistoricalLegalPerson> byName = new java.util.HashMap<>();
java.util.Map<String, CreditHistoricalLegalPerson> byNameDate = new java.util.HashMap<>();
for (CreditHistoricalLegalPerson existing : existingList) {
if (existing == null || ImportHelper.isBlank(existing.getName())) {
continue;
}
String n = existing.getName().trim();
byName.putIfAbsent(n, existing);
String d = ImportHelper.isBlank(existing.getRegisterDate()) ? null : existing.getRegisterDate().trim();
if (d != null) {
byNameDate.putIfAbsent(n + "|" + d, existing);
}
}
List<CreditHistoricalLegalPerson> updates = new ArrayList<>();
List<CreditHistoricalLegalPerson> inserts = new ArrayList<>();
for (CreditHistoricalLegalPerson it : chunkItems) {
if (it == null || ImportHelper.isBlank(it.getName())) {
continue;
}
String n = it.getName().trim();
CreditHistoricalLegalPerson existing;
if (!ImportHelper.isBlank(it.getRegisterDate())) {
String d = it.getRegisterDate().trim();
existing = byNameDate.get(n + "|" + d);
} else {
existing = byName.get(n);
}
if (existing != null) {
it.setId(existing.getId());
updates.add(it);
} else {
inserts.add(it);
}
}
if (!updates.isEmpty()) {
creditHistoricalLegalPersonService.updateBatchById(updates, mpBatchSize);
}
if (!inserts.isEmpty()) {
creditHistoricalLegalPersonService.saveBatch(inserts, mpBatchSize);
}
return updates.size() + inserts.size();
},
(rowItem, rowNumber) -> {
boolean saved = creditHistoricalLegalPersonService.save(rowItem);
if (!saved) {
CreditHistoricalLegalPerson existing = creditHistoricalLegalPersonService.lambdaQuery()
.eq(CreditHistoricalLegalPerson::getName, rowItem.getName())
.eq(!ImportHelper.isBlank(rowItem.getRegisterDate()), CreditHistoricalLegalPerson::getRegisterDate, rowItem.getRegisterDate())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditHistoricalLegalPersonService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditHistoricalLegalPerson::getName,
"",
errorMessages
);
}

View File

@@ -32,7 +32,6 @@ import java.nio.file.Files;
import java.util.Locale;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -114,7 +113,7 @@ public class CreditJudgmentDebtorController extends BaseController {
@Operation(summary = "删除被执行人")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditJudgmentDebtorService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditJudgmentDebtor.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -147,7 +146,7 @@ public class CreditJudgmentDebtorController extends BaseController {
@Operation(summary = "批量删除被执行人")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditJudgmentDebtorService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditJudgmentDebtor.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -241,7 +240,7 @@ public class CreditJudgmentDebtorController extends BaseController {
/**
* 批量导入历史被执行人(写入被执行人表 credit_judgment_debtor仅解析“历史被执行人”选项卡
* 规则:案号相同则更新;案号不存在则插入;导入文件内案号重复时取最后一条覆盖
* 规则:使用数据库唯一索引约束,重复数据不导入
*/
@PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:save')")
@Operation(summary = "批量导入历史被执行人")
@@ -369,9 +368,9 @@ public class CreditJudgmentDebtorController extends BaseController {
: (param.getOccurrenceTime() != null ? param.getOccurrenceTime().trim() : null);
entity.setOccurrenceTime(occurrenceTime);
// 兼容企查查历史被执行人:执行标的(元) / 执行法院
String amount = !ImportHelper.isBlank(param.getInvolvedAmount())
? param.getInvolvedAmount()
: param.getInvolvedAmountQcc();
String amount = !ImportHelper.isBlank(param.getInvolvedAmountQcc())
? param.getInvolvedAmountQcc()
: param.getInvolvedAmount();
if (amount != null) {
amount = amount.trim();
}
@@ -528,10 +527,10 @@ public class CreditJudgmentDebtorController extends BaseController {
Map<String, String> urlByName1 = ExcelImportSupport.readHyperlinksByHeaderKey(excelFile, usedSheetIndex, usedTitleRows, usedHeadRows, "被执行人");
String prefix = ImportHelper.isBlank(fileLabel) ? "" : "" + fileLabel + "";
// 同案号多条:以导入文件中“最后一条”为准(视为最新),避免批处理中重复 upsert。
LinkedHashMap<String, CreditJudgmentDebtor> latestByCaseNumber = new LinkedHashMap<>();
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>();
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditJudgmentDebtor> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditJudgmentDebtorImportParam param = list.get(i);
@@ -582,35 +581,19 @@ public class CreditJudgmentDebtorController extends BaseController {
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item);
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber);
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += persistHistoryImportChunk(chunkItems, chunkRowNumbers, prefix, mpBatchSize, errorMessages);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add(prefix + "" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (latestByCaseNumber.isEmpty()) {
return new ImportOutcome(true, 0, errorMessages, touchedCompanyIds);
}
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditJudgmentDebtor> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (Map.Entry<String, CreditJudgmentDebtor> entry : latestByCaseNumber.entrySet()) {
String caseNumber = entry.getKey();
CreditJudgmentDebtor item = entry.getValue();
Integer rowNo = latestRowByCaseNumber.get(caseNumber);
chunkItems.add(item);
chunkRowNumbers.add(rowNo != null ? rowNo : -1);
if (chunkItems.size() >= chunkSize) {
successCount += persistHistoryImportChunk(chunkItems, chunkRowNumbers, prefix, mpBatchSize, errorMessages);
chunkItems.clear();
chunkRowNumbers.clear();
}
}
if (!chunkItems.isEmpty()) {
successCount += persistHistoryImportChunk(chunkItems, chunkRowNumbers, prefix, mpBatchSize, errorMessages);
}
@@ -623,101 +606,15 @@ public class CreditJudgmentDebtorController extends BaseController {
String prefix,
int mpBatchSize,
List<String> errorMessages) {
if (CollectionUtils.isEmpty(items)) {
return 0;
}
try {
return batchImportSupport.runInNewTx(() -> {
List<String> keys = new ArrayList<>(items.size());
for (CreditJudgmentDebtor item : items) {
if (item == null || ImportHelper.isBlank(item.getCaseNumber())) {
continue;
}
keys.add(item.getCaseNumber().trim());
}
Map<String, CreditJudgmentDebtor> existingByCaseNumber = new java.util.HashMap<>();
if (!keys.isEmpty()) {
List<CreditJudgmentDebtor> existingList = creditJudgmentDebtorService.lambdaQuery()
.in(CreditJudgmentDebtor::getCaseNumber, keys)
.select(CreditJudgmentDebtor::getId, CreditJudgmentDebtor::getCaseNumber, CreditJudgmentDebtor::getRecommend)
.list();
for (CreditJudgmentDebtor existing : existingList) {
if (existing == null || ImportHelper.isBlank(existing.getCaseNumber())) {
continue;
}
existingByCaseNumber.putIfAbsent(existing.getCaseNumber().trim(), existing);
}
}
List<CreditJudgmentDebtor> updates = new ArrayList<>();
List<CreditJudgmentDebtor> inserts = new ArrayList<>();
for (CreditJudgmentDebtor item : items) {
if (item == null || ImportHelper.isBlank(item.getCaseNumber())) {
continue;
}
String caseNumber = item.getCaseNumber().trim();
CreditJudgmentDebtor existing = existingByCaseNumber.get(caseNumber);
if (existing != null && existing.getId() != null) {
// 覆盖更新recommend 记录“被更新次数”,每次更新 +1
item.setId(existing.getId());
Integer old = existing.getRecommend();
item.setRecommend(old == null ? 1 : old + 1);
updates.add(item);
} else {
if (item.getRecommend() == null) {
item.setRecommend(0);
}
inserts.add(item);
}
}
if (!updates.isEmpty()) {
creditJudgmentDebtorService.updateBatchById(updates, mpBatchSize);
}
if (!inserts.isEmpty()) {
creditJudgmentDebtorService.saveBatch(inserts, mpBatchSize);
}
return updates.size() + inserts.size();
});
} catch (Exception batchException) {
int successCount = 0;
for (int i = 0; i < items.size(); i++) {
CreditJudgmentDebtor item = items.get(i);
int excelRowNumber = (excelRowNumbers != null && i < excelRowNumbers.size()) ? excelRowNumbers.get(i) : -1;
try {
int delta = batchImportSupport.runInNewTx(() -> {
if (item == null || ImportHelper.isBlank(item.getCaseNumber())) {
return 0;
}
String caseNumber = item.getCaseNumber().trim();
CreditJudgmentDebtor existing = creditJudgmentDebtorService.lambdaQuery()
.eq(CreditJudgmentDebtor::getCaseNumber, caseNumber)
.select(CreditJudgmentDebtor::getId, CreditJudgmentDebtor::getRecommend)
.one();
if (existing != null && existing.getId() != null) {
item.setId(existing.getId());
Integer old = existing.getRecommend();
item.setRecommend(old == null ? 1 : old + 1);
return creditJudgmentDebtorService.updateById(item) ? 1 : 0;
}
if (item.getRecommend() == null) {
item.setRecommend(0);
}
return creditJudgmentDebtorService.save(item) ? 1 : 0;
});
if (delta > 0) {
successCount += delta;
} else {
errorMessages.add(prefix + "" + excelRowNumber + "行:保存失败");
}
} catch (Exception e) {
errorMessages.add(prefix + "" + excelRowNumber + "行:" + e.getMessage());
}
}
return successCount;
}
return batchImportSupport.persistInsertOnlyChunk(
creditJudgmentDebtorService,
items,
excelRowNumbers,
mpBatchSize,
CreditJudgmentDebtor::getCaseNumber,
prefix,
errorMessages
);
}
private int persistImportChunk(List<CreditJudgmentDebtor> items,
@@ -725,54 +622,15 @@ public class CreditJudgmentDebtorController extends BaseController {
String prefix,
int mpBatchSize,
List<String> errorMessages) {
if (CollectionUtils.isEmpty(items)) {
return 0;
}
try {
return batchImportSupport.runInNewTx(() -> batchImportSupport.upsertBySingleKey(
creditJudgmentDebtorService,
items,
CreditJudgmentDebtor::getId,
CreditJudgmentDebtor::setId,
CreditJudgmentDebtor::getCaseNumber,
CreditJudgmentDebtor::getCaseNumber,
null,
mpBatchSize
));
} catch (Exception batchException) {
int successCount = 0;
for (int i = 0; i < items.size(); i++) {
CreditJudgmentDebtor item = items.get(i);
int excelRowNumber = (excelRowNumbers != null && i < excelRowNumbers.size()) ? excelRowNumbers.get(i) : -1;
try {
int delta = batchImportSupport.runInNewTx(() -> {
boolean saved = creditJudgmentDebtorService.save(item);
if (!saved) {
CreditJudgmentDebtor existing = creditJudgmentDebtorService.lambdaQuery()
.eq(CreditJudgmentDebtor::getCaseNumber, item.getCaseNumber())
.one();
if (existing != null) {
item.setId(existing.getId());
if (creditJudgmentDebtorService.updateById(item)) {
return 1;
}
}
} else {
return 1;
}
return 0;
});
if (delta > 0) {
successCount += delta;
} else {
errorMessages.add(prefix + "" + excelRowNumber + "行:保存失败");
}
} catch (Exception e) {
errorMessages.add(prefix + "" + excelRowNumber + "行:" + e.getMessage());
}
}
return successCount;
}
return batchImportSupport.persistInsertOnlyChunk(
creditJudgmentDebtorService,
items,
excelRowNumbers,
mpBatchSize,
CreditJudgmentDebtor::getCaseNumber,
prefix,
errorMessages
);
}
private ImportOutcome importFromZip(MultipartFile zipFile, Integer currentUserId, Integer currentTenantId, Integer companyId) throws Exception {

View File

@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditJudicialDocumentController extends BaseController {
@Operation(summary = "删除裁判文书司法大数据")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditJudicialDocumentService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditJudicialDocument.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditJudicialDocumentController extends BaseController {
@Operation(summary = "批量删除裁判文书司法大数据")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditJudicialDocumentService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditJudicialDocument.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -271,38 +270,13 @@ public class CreditJudicialDocumentController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditJudicialDocumentService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditJudicialDocumentService,
chunkItems,
CreditJudicialDocument::getId,
CreditJudicialDocument::setId,
CreditJudicialDocument::getCaseNumber,
CreditJudicialDocument::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditJudicialDocumentService.save(rowItem);
if (!saved) {
CreditJudicialDocument existing = creditJudicialDocumentService.lambdaQuery()
.eq(CreditJudicialDocument::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditJudicialDocumentService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditJudicialDocument::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
@@ -316,38 +290,13 @@ public class CreditJudicialDocumentController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditJudicialDocumentService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditJudicialDocumentService,
chunkItems,
CreditJudicialDocument::getId,
CreditJudicialDocument::setId,
CreditJudicialDocument::getCaseNumber,
CreditJudicialDocument::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditJudicialDocumentService.save(rowItem);
if (!saved) {
CreditJudicialDocument existing = creditJudicialDocumentService.lambdaQuery()
.eq(CreditJudicialDocument::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditJudicialDocumentService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditJudicialDocument::getCaseNumber,
"",
errorMessages
);
}
@@ -367,7 +316,7 @@ public class CreditJudicialDocumentController extends BaseController {
/**
* 批量导入历史裁判文书(仅解析“历史裁判文书”选项卡)
* 规则:案号相同则覆盖更新recommend++ 记录更新次数);案号不存在则插入。
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditJudicialDocument:save')")
@Operation(summary = "批量导入历史裁判文书司法大数据")
@@ -401,8 +350,10 @@ public class CreditJudicialDocumentController extends BaseController {
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
Map<String, String> urlByTitle = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "文书标题");
LinkedHashMap<String, CreditJudicialDocument> latestByCaseNumber = new LinkedHashMap<>();
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>();
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditJudicialDocument> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditJudicialDocumentImportParam param = list.get(i);
@@ -446,121 +397,42 @@ public class CreditJudicialDocumentController extends BaseController {
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item);
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber);
if (item.getRecommend() == null) {
item.setRecommend(0);
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditJudicialDocumentService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditJudicialDocument::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (latestByCaseNumber.isEmpty()) {
if (errorMessages.isEmpty()) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
return success("导入完成成功0条失败" + errorMessages.size() + "", errorMessages);
}
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditJudicialDocument> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (Map.Entry<String, CreditJudicialDocument> entry : latestByCaseNumber.entrySet()) {
String caseNumber = entry.getKey();
CreditJudicialDocument item = entry.getValue();
Integer rowNo = latestRowByCaseNumber.get(caseNumber);
chunkItems.add(item);
chunkRowNumbers.add(rowNo != null ? rowNo : -1);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditJudicialDocumentService,
chunkItems,
CreditJudicialDocument::getId,
CreditJudicialDocument::setId,
CreditJudicialDocument::getCaseNumber,
CreditJudicialDocument::getCaseNumber,
CreditJudicialDocument::getRecommend,
CreditJudicialDocument::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditJudicialDocumentService.save(rowItem);
if (!saved) {
CreditJudicialDocument existing = creditJudicialDocumentService.lambdaQuery()
.eq(CreditJudicialDocument::getCaseNumber, rowItem.getCaseNumber())
.select(CreditJudicialDocument::getId, CreditJudicialDocument::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditJudicialDocumentService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditJudicialDocumentService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditJudicialDocumentService,
chunkItems,
CreditJudicialDocument::getId,
CreditJudicialDocument::setId,
CreditJudicialDocument::getCaseNumber,
CreditJudicialDocument::getCaseNumber,
CreditJudicialDocument::getRecommend,
CreditJudicialDocument::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditJudicialDocumentService.save(rowItem);
if (!saved) {
CreditJudicialDocument existing = creditJudicialDocumentService.lambdaQuery()
.eq(CreditJudicialDocument::getCaseNumber, rowItem.getCaseNumber())
.select(CreditJudicialDocument::getId, CreditJudicialDocument::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditJudicialDocumentService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditJudicialDocument::getCaseNumber,
"",
errorMessages
);
}
@@ -622,7 +494,7 @@ public class CreditJudicialDocumentController extends BaseController {
: param.getInvolvedAmount();
entity.setTitle(param.getTitle());
entity.setType(param.getType());
entity.setDocumentType(param.getDocumentType());
entity.setDataStatus(param.getDataStatus());
entity.setOtherPartiesThirdParty(param.getOtherPartiesThirdParty());
entity.setOccurrenceTime(param.getOccurrenceTime());

View File

@@ -103,7 +103,7 @@ public class CreditJudiciaryController extends BaseController {
@Operation(summary = "删除司法案件")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditJudiciaryService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditJudiciary.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -136,7 +136,7 @@ public class CreditJudiciaryController extends BaseController {
@Operation(summary = "批量删除司法案件")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditJudiciaryService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditJudiciary.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -294,83 +294,39 @@ public class CreditJudiciaryController extends BaseController {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditJudiciaryService,
chunkItems,
CreditJudiciary::getId,
CreditJudiciary::setId,
CreditJudiciary::getName,
CreditJudiciary::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditJudiciaryService.save(rowItem);
if (!saved) {
CreditJudiciary existing = creditJudiciaryService.getByName(rowItem.getName());
if (existing != null) {
rowItem.setId(existing.getId());
if (creditJudiciaryService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
errorMessages.add("" + rowNumber + "行:保存失败");
return false;
},
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditJudiciaryService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditJudiciary::getCode,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows;
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditJudiciaryService,
chunkItems,
CreditJudiciary::getId,
CreditJudiciary::setId,
CreditJudiciary::getName,
CreditJudiciary::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditJudiciaryService.save(rowItem);
if (!saved) {
CreditJudiciary existing = creditJudiciaryService.getByName(rowItem.getName());
if (existing != null) {
rowItem.setId(existing.getId());
if (creditJudiciaryService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
errorMessages.add("" + rowNumber + "行:保存失败");
return false;
},
errorMessages
);
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditJudiciaryService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditJudiciary::getCode,
"",
errorMessages
);
}
creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.JUDICIARY, touchedCompanyIds);

View File

@@ -104,7 +104,7 @@ public class CreditMediationController extends BaseController {
@Operation(summary = "删除诉前调解司法大数据")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditMediationService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditMediation.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +137,7 @@ public class CreditMediationController extends BaseController {
@Operation(summary = "批量删除诉前调解司法大数据")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditMediationService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditMediation.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -264,38 +264,13 @@ public class CreditMediationController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditMediationService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditMediationService,
chunkItems,
CreditMediation::getId,
CreditMediation::setId,
CreditMediation::getCaseNumber,
CreditMediation::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditMediationService.save(rowItem);
if (!saved) {
CreditMediation existing = creditMediationService.lambdaQuery()
.eq(CreditMediation::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditMediationService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditMediation::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
@@ -309,38 +284,13 @@ public class CreditMediationController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditMediationService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditMediationService,
chunkItems,
CreditMediation::getId,
CreditMediation::setId,
CreditMediation::getCaseNumber,
CreditMediation::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditMediationService.save(rowItem);
if (!saved) {
CreditMediation existing = creditMediationService.lambdaQuery()
.eq(CreditMediation::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditMediationService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditMediation::getCaseNumber,
"",
errorMessages
);
}
@@ -358,6 +308,134 @@ public class CreditMediationController extends BaseController {
}
}
/**
* 批量导入历史诉前调解(仅解析“历史诉前调解”选项卡)
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditMediation:save')")
@Operation(summary = "批量导入历史诉前调解")
@PostMapping("/import/history")
public ApiResult<List<String>> importHistoryBatch(@RequestParam("file") MultipartFile file,
@RequestParam(value = "companyId", required = false) Integer companyId) {
List<String> errorMessages = new ArrayList<>();
int successCount = 0;
Set<Integer> touchedCompanyIds = new HashSet<>();
try {
int sheetIndex = ExcelImportSupport.findSheetIndex(file, "历史诉前调解");
if (sheetIndex < 0) {
return fail("未读取到数据,请确认文件中存在“历史诉前调解”选项卡且表头与示例格式一致", null);
}
ExcelImportSupport.ImportResult<CreditMediationImportParam> importResult = ExcelImportSupport.read(
file, CreditMediationImportParam.class, this::isEmptyImportRow, sheetIndex);
List<CreditMediationImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows();
int usedSheetIndex = importResult.getSheetIndex();
if (CollectionUtils.isEmpty(list)) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
User loginUser = getLoginUser();
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditMediation> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditMediationImportParam param = list.get(i);
int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows;
try {
CreditMediation item = convertImportParamToEntity(param);
if (item.getCaseNumber() != null) {
item.setCaseNumber(item.getCaseNumber().trim());
}
if (ImportHelper.isBlank(item.getCaseNumber())) {
errorMessages.add("" + excelRowNumber + "行:案号不能为空");
continue;
}
String link = urlByCaseNumber.get(item.getCaseNumber());
if (!ImportHelper.isBlank(link)) {
item.setUrl(link.trim());
}
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
}
if (item.getTenantId() == null && currentTenantId != null) {
item.setTenantId(currentTenantId);
}
if (item.getStatus() == null) {
item.setStatus(0);
}
if (item.getDeleted() == null) {
item.setDeleted(0);
}
if (item.getRecommend() == null) {
item.setRecommend(0);
}
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditMediationService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditMediation::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditMediationService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditMediation::getCaseNumber,
"",
errorMessages
);
}
creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.MEDIATION, touchedCompanyIds);
if (errorMessages.isEmpty()) {
return success("成功导入" + successCount + "条数据", null);
}
return success("导入完成,成功" + successCount + "条,失败" + errorMessages.size() + "", errorMessages);
} catch (Exception e) {
e.printStackTrace();
return fail("导入失败:" + e.getMessage(), null);
}
}
/**
* 下载诉前调解导入模板
*/

View File

@@ -0,0 +1,178 @@
package com.gxwebsoft.credit.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.credit.service.CreditMpCustomerService;
import com.gxwebsoft.credit.entity.CreditMpCustomer;
import com.gxwebsoft.credit.param.CreditMpCustomerParam;
import com.gxwebsoft.credit.param.BatchFollowStepApprovalDTO;
import com.gxwebsoft.credit.param.EndFollowProcessDTO;
import com.gxwebsoft.credit.param.FollowStepApprovalDTO;
import com.gxwebsoft.credit.param.FollowStepQueryDTO;
import com.gxwebsoft.credit.vo.FollowStatisticsDTO;
import com.gxwebsoft.credit.vo.PendingApprovalStepVO;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 小程序端客户控制器
*
* @author 科技小王子
* @since 2026-03-16 20:59:17
*/
@Tag(name = "小程序端客户管理")
@RestController
@RequestMapping("/api/credit/credit-mp-customer")
public class CreditMpCustomerController extends BaseController {
@Resource
private CreditMpCustomerService creditMpCustomerService;
@PreAuthorize("hasAuthority('credit:creditMpCustomer:list')")
@Operation(summary = "分页查询小程序端客户")
@GetMapping("/page")
public ApiResult<PageResult<CreditMpCustomer>> page(CreditMpCustomerParam param) {
// 使用关联查询
return success(creditMpCustomerService.pageRel(param));
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:list')")
@Operation(summary = "查询全部小程序端客户")
@GetMapping()
public ApiResult<List<CreditMpCustomer>> list(CreditMpCustomerParam param) {
// 使用关联查询
return success(creditMpCustomerService.listRel(param));
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:list')")
@Operation(summary = "根据id查询小程序端客户")
@GetMapping("/{id}")
public ApiResult<CreditMpCustomer> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(creditMpCustomerService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:save')")
@OperationLog
@Operation(summary = "添加小程序端客户")
@PostMapping()
public ApiResult<?> save(@RequestBody CreditMpCustomer creditMpCustomer) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
creditMpCustomer.setUserId(loginUser.getUserId());
}
if (creditMpCustomerService.save(creditMpCustomer)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:update')")
@OperationLog
@Operation(summary = "修改小程序端客户")
@PutMapping()
public ApiResult<?> update(@RequestBody CreditMpCustomer creditMpCustomer) {
if (creditMpCustomerService.updateById(creditMpCustomer)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:remove')")
@OperationLog
@Operation(summary = "删除小程序端客户")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditMpCustomerService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:save')")
@OperationLog
@Operation(summary = "批量添加小程序端客户")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<CreditMpCustomer> list) {
if (creditMpCustomerService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:update')")
@OperationLog
@Operation(summary = "批量修改小程序端客户")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<CreditMpCustomer> batchParam) {
if (batchParam.update(creditMpCustomerService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:remove')")
@OperationLog
@Operation(summary = "批量删除小程序端客户")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditMpCustomerService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:update')")
@OperationLog
@Operation(summary = "审核跟进步骤")
@PostMapping("/approve-follow-step")
public ApiResult<?> approveFollowStep(@RequestBody FollowStepApprovalDTO dto) {
creditMpCustomerService.approveFollowStep(dto);
return success("审核成功");
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:update')")
@OperationLog
@Operation(summary = "批量审核跟进步骤")
@PostMapping("/batch-approve-follow-steps")
public ApiResult<?> batchApproveFollowSteps(@RequestBody BatchFollowStepApprovalDTO dto) {
creditMpCustomerService.batchApproveFollowSteps(dto);
return success("批量审核成功");
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:list')")
@Operation(summary = "获取待审核的跟进步骤")
@GetMapping("/pending-approval-steps")
public ApiResult<List<PendingApprovalStepVO>> getPendingApprovalSteps(FollowStepQueryDTO query) {
List<PendingApprovalStepVO> list = creditMpCustomerService.getPendingApprovalSteps(query);
return success(list);
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:list')")
@Operation(summary = "获取客户跟进统计")
@GetMapping("/follow-statistics/{customerId}")
public ApiResult<FollowStatisticsDTO> getFollowStatistics(@PathVariable Long customerId) {
FollowStatisticsDTO statistics = creditMpCustomerService.getFollowStatistics(customerId);
return success(statistics);
}
@PreAuthorize("hasAuthority('credit:creditMpCustomer:update')")
@OperationLog
@Operation(summary = "结束客户跟进流程")
@PostMapping("/end-follow-process")
public ApiResult<?> endFollowProcess(@RequestBody EndFollowProcessDTO dto) {
creditMpCustomerService.endFollowProcess(dto);
return success("流程结束成功");
}
}

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditNearbyCompany;
import com.gxwebsoft.credit.param.CreditNearbyCompanyImportParam;
import com.gxwebsoft.credit.param.CreditNearbyCompanyParam;
@@ -104,7 +105,7 @@ public class CreditNearbyCompanyController extends BaseController {
@Operation(summary = "删除附近企业")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditNearbyCompanyService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditNearbyCompany.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditNearbyCompanyController extends BaseController {
@Operation(summary = "批量删除附近企业")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditNearbyCompanyService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditNearbyCompany.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditNearbyCompanyController extends BaseController {
CreditNearbyCompany::getName,
CreditNearbyCompany::getCompanyId,
CreditNearbyCompany::setCompanyId,
CreditNearbyCompany::setCompanyName,
CreditNearbyCompany::getHasData,
CreditNearbyCompany::setHasData,
CreditNearbyCompany::getTenantId,
@@ -213,6 +215,11 @@ public class CreditNearbyCompanyController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCode = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "统一社会信用代码");
Map<String, String> urlByName = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "企业名称");
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
// 避免逐行写库:按批处理,显著降低 SQL 次数与事务开销
final int chunkSize = 500;
@@ -243,6 +250,9 @@ public class CreditNearbyCompanyController extends BaseController {
}
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
@@ -308,58 +318,22 @@ public class CreditNearbyCompanyController extends BaseController {
Integer tenantId,
int mpBatchSize,
List<String> errorMessages) {
return batchImportSupport.persistChunkWithFallback(
return batchImportSupport.persistInsertOnlyChunk(
creditNearbyCompanyService,
items,
excelRowNumbers,
() -> batchImportSupport.upsertByCodeOrName(
creditNearbyCompanyService,
items,
CreditNearbyCompany::getId,
CreditNearbyCompany::setId,
CreditNearbyCompany::getCode,
CreditNearbyCompany::getCode,
CreditNearbyCompany::getName,
CreditNearbyCompany::getName,
wrapper -> {
if (companyId != null) {
wrapper.eq(CreditNearbyCompany::getCompanyId, companyId);
}
if (parentId != null) {
wrapper.eq(CreditNearbyCompany::getParentId, parentId);
}
if (type != null) {
wrapper.eq(CreditNearbyCompany::getType, type);
}
if (tenantId != null) {
wrapper.eq(CreditNearbyCompany::getTenantId, tenantId);
}
},
mpBatchSize
),
(item, excelRowNumber) -> {
boolean saved = creditNearbyCompanyService.save(item);
if (!saved) {
CreditNearbyCompany existing = creditNearbyCompanyService.lambdaQuery()
.eq(!ImportHelper.isBlank(item.getCode()), CreditNearbyCompany::getCode, item.getCode())
.eq(ImportHelper.isBlank(item.getCode()), CreditNearbyCompany::getName, item.getName())
.eq(item.getCompanyId() != null, CreditNearbyCompany::getCompanyId, item.getCompanyId())
.eq(item.getParentId() != null, CreditNearbyCompany::getParentId, item.getParentId())
.eq(item.getType() != null, CreditNearbyCompany::getType, item.getType())
.eq(item.getTenantId() != null, CreditNearbyCompany::getTenantId, item.getTenantId())
.one();
if (existing != null) {
item.setId(existing.getId());
if (creditNearbyCompanyService.updateById(item)) {
return true;
}
}
} else {
return true;
mpBatchSize,
it -> {
if (it == null) {
return null;
}
String prefix = excelRowNumber > 0 ? ("" + excelRowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
String code = it.getCode();
if (code != null && !code.trim().isEmpty()) {
return code;
}
return it.getName();
},
"",
errorMessages
);
}
@@ -444,7 +418,6 @@ public class CreditNearbyCompanyController extends BaseController {
entity.setRegistrationStatus(param.getRegistrationStatus());
entity.setLegalPerson(param.getLegalPerson());
entity.setRegisteredCapital(param.getRegisteredCapital());
entity.setPaidinCapital(param.getPaidinCapital());
entity.setEstablishDate(param.getEstablishDate());
entity.setCode(param.getCode());
entity.setAddress(param.getAddress());
@@ -453,21 +426,35 @@ public class CreditNearbyCompanyController extends BaseController {
entity.setProvince(param.getProvince());
entity.setCity(param.getCity());
entity.setRegion(param.getRegion());
entity.setTaxpayerCode(param.getTaxpayerCode());
entity.setRegistrationNumber(param.getRegistrationNumber());
entity.setOrganizationalCode(param.getOrganizationalCode());
entity.setNumberOfInsuredPersons(param.getNumberOfInsuredPersons());
entity.setAnnualReport(param.getAnnualReport());
entity.setDomain(param.getDomain());
entity.setBusinessTerm(param.getBusinessTerm());
entity.setNationalStandardIndustryCategories(param.getNationalStandardIndustryCategories());
entity.setNationalStandardIndustryCategories2(param.getNationalStandardIndustryCategories2());
entity.setNationalStandardIndustryCategories3(param.getNationalStandardIndustryCategories3());
entity.setNationalStandardIndustryCategories4(param.getNationalStandardIndustryCategories4());
entity.setFormerName(param.getFormerName());
entity.setEnglishName(param.getEnglishName());
entity.setMailingAddress(param.getMailingAddress());
entity.setMailingEmail(param.getMailingEmail());
entity.setTel(param.getTel());
entity.setPostalCode(param.getPostalCode());
entity.setNationalStandardIndustryCategories5(param.getNationalStandardIndustryCategories5());
entity.setNationalStandardIndustryCategories6(param.getNationalStandardIndustryCategories6());
entity.setNationalStandardIndustryCategories7(param.getNationalStandardIndustryCategories7());
entity.setNationalStandardIndustryCategories8(param.getNationalStandardIndustryCategories8());
entity.setType(param.getType());
entity.setInstitutionType(param.getInstitutionType());
entity.setCompanySize(param.getCompanySize());
entity.setRegistrationAuthority(param.getRegistrationAuthority());
entity.setTaxpayerQualification(param.getTaxpayerQualification());
entity.setLatestAnnualReportYear(param.getLatestAnnualReportYear());
entity.setLatestAnnualReportOnOperatingRevenue(param.getLatestAnnualReportOnOperatingRevenue());
entity.setEnterpriseScoreCheck(param.getEnterpriseScoreCheck());
entity.setCreditRating(param.getCreditRating());
entity.setCechnologyScore(param.getCechnologyScore());
entity.setCechnologyLevel(param.getCechnologyLevel());
entity.setSmallEnterprise(param.getSmallEnterprise());
entity.setCompanyProfile(param.getCompanyProfile());
entity.setNatureOfBusiness(param.getNatureOfBusiness());
entity.setComments(param.getComments());
entity.setMoreEmail(param.getMoreEmail());
entity.setMoreTel(param.getMoreTel());
return entity;
}

View File

@@ -104,7 +104,7 @@ public class CreditPatentController extends BaseController {
@Operation(summary = "删除专利")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditPatentService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditPatent.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +137,7 @@ public class CreditPatentController extends BaseController {
@Operation(summary = "批量删除专利")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditPatentService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditPatent.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -195,8 +195,13 @@ public class CreditPatentController extends BaseController {
Set<Integer> touchedCompanyIds = new HashSet<>();
try {
ExcelImportSupport.ImportResult<CreditPatentImportParam> importResult = ExcelImportSupport.readAnySheet(
file, CreditPatentImportParam.class, this::isEmptyImportRow);
// 单企业表通常是多 sheet专利页签名一般为“专利”
int preferredSheetIndex = ExcelImportSupport.findSheetIndex(file, "专利", 0);
ExcelImportSupport.ImportResult<CreditPatentImportParam> importResult = ExcelImportSupport.read(
file, CreditPatentImportParam.class, this::isEmptyImportRow, preferredSheetIndex);
if (CollectionUtils.isEmpty(importResult.getData())) {
importResult = ExcelImportSupport.readAnySheet(file, CreditPatentImportParam.class, this::isEmptyImportRow);
}
List<CreditPatentImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows();
@@ -264,38 +269,13 @@ public class CreditPatentController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditPatentService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditPatentService,
chunkItems,
CreditPatent::getId,
CreditPatent::setId,
CreditPatent::getRegisterNo,
CreditPatent::getRegisterNo,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditPatentService.save(rowItem);
if (!saved) {
CreditPatent existing = creditPatentService.lambdaQuery()
.eq(CreditPatent::getRegisterNo, rowItem.getRegisterNo())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditPatentService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditPatent::getRegisterNo,
"",
errorMessages
);
chunkItems.clear();
@@ -309,38 +289,13 @@ public class CreditPatentController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditPatentService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditPatentService,
chunkItems,
CreditPatent::getId,
CreditPatent::setId,
CreditPatent::getRegisterNo,
CreditPatent::getRegisterNo,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditPatentService.save(rowItem);
if (!saved) {
CreditPatent existing = creditPatentService.lambdaQuery()
.eq(CreditPatent::getRegisterNo, rowItem.getRegisterNo())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditPatentService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditPatent::getRegisterNo,
"",
errorMessages
);
}
@@ -395,7 +350,16 @@ public class CreditPatentController extends BaseController {
if (isImportHeaderRow(param)) {
return true;
}
return ImportHelper.isBlank(param.getPublicNo());
return ImportHelper.isBlank(param.getName())
&& ImportHelper.isBlank(param.getType())
&& ImportHelper.isBlank(param.getStatusText())
&& ImportHelper.isBlank(param.getRegisterNo())
&& ImportHelper.isBlank(param.getRegisterDate())
&& ImportHelper.isBlank(param.getPublicNo())
&& ImportHelper.isBlank(param.getPublicDate())
&& ImportHelper.isBlank(param.getInventor())
&& ImportHelper.isBlank(param.getPatentApplicant())
&& ImportHelper.isBlank(param.getComments());
}
private boolean isImportHeaderRow(CreditPatentImportParam param) {

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditRiskRelation;
import com.gxwebsoft.credit.param.CreditRiskRelationImportParam;
import com.gxwebsoft.credit.param.CreditRiskRelationParam;
@@ -104,7 +105,7 @@ public class CreditRiskRelationController extends BaseController {
@Operation(summary = "删除风险关系表")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditRiskRelationService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditRiskRelation.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditRiskRelationController extends BaseController {
@Operation(summary = "批量删除风险关系表")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditRiskRelationService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditRiskRelation.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditRiskRelationController extends BaseController {
CreditRiskRelation::getMainBodyName,
CreditRiskRelation::getCompanyId,
CreditRiskRelation::setCompanyId,
CreditRiskRelation::setCompanyName,
CreditRiskRelation::getHasData,
CreditRiskRelation::setHasData,
CreditRiskRelation::getTenantId,
@@ -209,6 +211,11 @@ public class CreditRiskRelationController extends BaseController {
User loginUser = getLoginUser();
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -222,6 +229,9 @@ public class CreditRiskRelationController extends BaseController {
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
@@ -252,38 +262,13 @@ public class CreditRiskRelationController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditRiskRelationService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditRiskRelationService,
chunkItems,
CreditRiskRelation::getId,
CreditRiskRelation::setId,
CreditRiskRelation::getMainBodyName,
CreditRiskRelation::getMainBodyName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditRiskRelationService.save(rowItem);
if (!saved) {
CreditRiskRelation existing = creditRiskRelationService.lambdaQuery()
.eq(CreditRiskRelation::getMainBodyName, rowItem.getMainBodyName())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditRiskRelationService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditRiskRelation::getMainBodyName,
"",
errorMessages
);
chunkItems.clear();
@@ -297,38 +282,13 @@ public class CreditRiskRelationController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditRiskRelationService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditRiskRelationService,
chunkItems,
CreditRiskRelation::getId,
CreditRiskRelation::setId,
CreditRiskRelation::getMainBodyName,
CreditRiskRelation::getMainBodyName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditRiskRelationService.save(rowItem);
if (!saved) {
CreditRiskRelation existing = creditRiskRelationService.lambdaQuery()
.eq(CreditRiskRelation::getMainBodyName, rowItem.getMainBodyName())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditRiskRelationService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditRiskRelation::getMainBodyName,
"",
errorMessages
);
}

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditSupplier;
import com.gxwebsoft.credit.param.CreditSupplierImportParam;
import com.gxwebsoft.credit.param.CreditSupplierParam;
@@ -104,7 +105,7 @@ public class CreditSupplierController extends BaseController {
@Operation(summary = "删除供应商")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditSupplierService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditSupplier.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditSupplierController extends BaseController {
@Operation(summary = "批量删除供应商")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditSupplierService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditSupplier.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditSupplierController extends BaseController {
CreditSupplier::getSupplier,
CreditSupplier::getCompanyId,
CreditSupplier::setCompanyId,
CreditSupplier::setCompanyName,
CreditSupplier::getHasData,
CreditSupplier::setHasData,
CreditSupplier::getTenantId,
@@ -211,6 +213,11 @@ public class CreditSupplierController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlBySupplier = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "供应商");
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -230,6 +237,9 @@ public class CreditSupplierController extends BaseController {
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
@@ -260,38 +270,13 @@ public class CreditSupplierController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditSupplierService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditSupplierService,
chunkItems,
CreditSupplier::getId,
CreditSupplier::setId,
CreditSupplier::getSupplier,
CreditSupplier::getSupplier,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditSupplierService.save(rowItem);
if (!saved) {
CreditSupplier existing = creditSupplierService.lambdaQuery()
.eq(CreditSupplier::getSupplier, rowItem.getSupplier())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditSupplierService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditSupplier::getSupplier,
"",
errorMessages
);
chunkItems.clear();
@@ -305,38 +290,13 @@ public class CreditSupplierController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditSupplierService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditSupplierService,
chunkItems,
CreditSupplier::getId,
CreditSupplier::setId,
CreditSupplier::getSupplier,
CreditSupplier::getSupplier,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditSupplierService.save(rowItem);
if (!saved) {
CreditSupplier existing = creditSupplierService.lambdaQuery()
.eq(CreditSupplier::getSupplier, rowItem.getSupplier())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditSupplierService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditSupplier::getSupplier,
"",
errorMessages
);
}

View File

@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditSuspectedRelationship;
import com.gxwebsoft.credit.param.CreditSuspectedRelationshipImportParam;
import com.gxwebsoft.credit.param.CreditSuspectedRelationshipParam;
@@ -104,7 +105,7 @@ public class CreditSuspectedRelationshipController extends BaseController {
@Operation(summary = "删除疑似关系")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditSuspectedRelationshipService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditSuspectedRelationship.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditSuspectedRelationshipController extends BaseController {
@Operation(summary = "批量删除疑似关系")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditSuspectedRelationshipService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditSuspectedRelationship.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditSuspectedRelationshipController extends BaseController {
CreditSuspectedRelationship::getName,
CreditSuspectedRelationship::getCompanyId,
CreditSuspectedRelationship::setCompanyId,
CreditSuspectedRelationship::setCompanyName,
CreditSuspectedRelationship::getHasData,
CreditSuspectedRelationship::setHasData,
CreditSuspectedRelationship::getTenantId,
@@ -210,6 +212,11 @@ public class CreditSuspectedRelationshipController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "企业名称");
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -229,6 +236,9 @@ public class CreditSuspectedRelationshipController extends BaseController {
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId);
@@ -263,99 +273,29 @@ public class CreditSuspectedRelationshipController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditSuspectedRelationshipService,
chunkItems,
chunkRowNumbers,
() -> {
List<String> names = new ArrayList<>(chunkItems.size());
List<String> relatedParties = new ArrayList<>(chunkItems.size());
for (CreditSuspectedRelationship it : chunkItems) {
if (it == null) {
continue;
}
if (!ImportHelper.isBlank(it.getName())) {
names.add(it.getName().trim());
}
if (!ImportHelper.isBlank(it.getRelatedParty())) {
relatedParties.add(it.getRelatedParty().trim());
}
mpBatchSize,
it -> {
if (it == null) {
return null;
}
List<CreditSuspectedRelationship> existingList = (names.isEmpty() || relatedParties.isEmpty())
? new ArrayList<>()
: creditSuspectedRelationshipService.lambdaQuery()
.in(CreditSuspectedRelationship::getName, names)
.in(CreditSuspectedRelationship::getRelatedParty, relatedParties)
.list();
java.util.Map<String, CreditSuspectedRelationship> byNameRelated = new java.util.HashMap<>();
java.util.Map<String, CreditSuspectedRelationship> byNameRelatedType = new java.util.HashMap<>();
for (CreditSuspectedRelationship existing : existingList) {
if (existing == null
|| ImportHelper.isBlank(existing.getName())
|| ImportHelper.isBlank(existing.getRelatedParty())) {
continue;
}
String n = existing.getName().trim();
String r = existing.getRelatedParty().trim();
byNameRelated.putIfAbsent(n + "|" + r, existing);
if (!ImportHelper.isBlank(existing.getType())) {
byNameRelatedType.putIfAbsent(n + "|" + r + "|" + existing.getType().trim(), existing);
}
String n = it.getName();
String r = it.getRelatedParty();
if (n != null) {
n = n.trim();
}
List<CreditSuspectedRelationship> updates = new ArrayList<>();
List<CreditSuspectedRelationship> inserts = new ArrayList<>();
for (CreditSuspectedRelationship it : chunkItems) {
if (it == null
|| ImportHelper.isBlank(it.getName())
|| ImportHelper.isBlank(it.getRelatedParty())) {
continue;
}
String n = it.getName().trim();
String r = it.getRelatedParty().trim();
CreditSuspectedRelationship existing;
if (!ImportHelper.isBlank(it.getType())) {
existing = byNameRelatedType.get(n + "|" + r + "|" + it.getType().trim());
} else {
existing = byNameRelated.get(n + "|" + r);
}
if (existing != null) {
it.setId(existing.getId());
updates.add(it);
} else {
inserts.add(it);
}
if (r != null) {
r = r.trim();
}
if (!updates.isEmpty()) {
creditSuspectedRelationshipService.updateBatchById(updates, mpBatchSize);
if (n != null && !n.isEmpty() && r != null && !r.isEmpty()) {
return n + "->" + r;
}
if (!inserts.isEmpty()) {
creditSuspectedRelationshipService.saveBatch(inserts, mpBatchSize);
}
return updates.size() + inserts.size();
},
(rowItem, rowNumber) -> {
boolean saved = creditSuspectedRelationshipService.save(rowItem);
if (!saved) {
CreditSuspectedRelationship existing = creditSuspectedRelationshipService.lambdaQuery()
.eq(CreditSuspectedRelationship::getName, rowItem.getName())
.eq(CreditSuspectedRelationship::getRelatedParty, rowItem.getRelatedParty())
.eq(!ImportHelper.isBlank(rowItem.getType()), CreditSuspectedRelationship::getType, rowItem.getType())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditSuspectedRelationshipService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
return n;
},
"",
errorMessages
);
chunkItems.clear();
@@ -369,99 +309,29 @@ public class CreditSuspectedRelationshipController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditSuspectedRelationshipService,
chunkItems,
chunkRowNumbers,
() -> {
List<String> names = new ArrayList<>(chunkItems.size());
List<String> relatedParties = new ArrayList<>(chunkItems.size());
for (CreditSuspectedRelationship it : chunkItems) {
if (it == null) {
continue;
}
if (!ImportHelper.isBlank(it.getName())) {
names.add(it.getName().trim());
}
if (!ImportHelper.isBlank(it.getRelatedParty())) {
relatedParties.add(it.getRelatedParty().trim());
}
mpBatchSize,
it -> {
if (it == null) {
return null;
}
List<CreditSuspectedRelationship> existingList = (names.isEmpty() || relatedParties.isEmpty())
? new ArrayList<>()
: creditSuspectedRelationshipService.lambdaQuery()
.in(CreditSuspectedRelationship::getName, names)
.in(CreditSuspectedRelationship::getRelatedParty, relatedParties)
.list();
java.util.Map<String, CreditSuspectedRelationship> byNameRelated = new java.util.HashMap<>();
java.util.Map<String, CreditSuspectedRelationship> byNameRelatedType = new java.util.HashMap<>();
for (CreditSuspectedRelationship existing : existingList) {
if (existing == null
|| ImportHelper.isBlank(existing.getName())
|| ImportHelper.isBlank(existing.getRelatedParty())) {
continue;
}
String n = existing.getName().trim();
String r = existing.getRelatedParty().trim();
byNameRelated.putIfAbsent(n + "|" + r, existing);
if (!ImportHelper.isBlank(existing.getType())) {
byNameRelatedType.putIfAbsent(n + "|" + r + "|" + existing.getType().trim(), existing);
}
String n = it.getName();
String r = it.getRelatedParty();
if (n != null) {
n = n.trim();
}
List<CreditSuspectedRelationship> updates = new ArrayList<>();
List<CreditSuspectedRelationship> inserts = new ArrayList<>();
for (CreditSuspectedRelationship it : chunkItems) {
if (it == null
|| ImportHelper.isBlank(it.getName())
|| ImportHelper.isBlank(it.getRelatedParty())) {
continue;
}
String n = it.getName().trim();
String r = it.getRelatedParty().trim();
CreditSuspectedRelationship existing;
if (!ImportHelper.isBlank(it.getType())) {
existing = byNameRelatedType.get(n + "|" + r + "|" + it.getType().trim());
} else {
existing = byNameRelated.get(n + "|" + r);
}
if (existing != null) {
it.setId(existing.getId());
updates.add(it);
} else {
inserts.add(it);
}
if (r != null) {
r = r.trim();
}
if (!updates.isEmpty()) {
creditSuspectedRelationshipService.updateBatchById(updates, mpBatchSize);
if (n != null && !n.isEmpty() && r != null && !r.isEmpty()) {
return n + "->" + r;
}
if (!inserts.isEmpty()) {
creditSuspectedRelationshipService.saveBatch(inserts, mpBatchSize);
}
return updates.size() + inserts.size();
},
(rowItem, rowNumber) -> {
boolean saved = creditSuspectedRelationshipService.save(rowItem);
if (!saved) {
CreditSuspectedRelationship existing = creditSuspectedRelationshipService.lambdaQuery()
.eq(CreditSuspectedRelationship::getName, rowItem.getName())
.eq(CreditSuspectedRelationship::getRelatedParty, rowItem.getRelatedParty())
.eq(!ImportHelper.isBlank(rowItem.getType()), CreditSuspectedRelationship::getType, rowItem.getType())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditSuspectedRelationshipService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
return n;
},
"",
errorMessages
);
}

View File

@@ -1,16 +1,14 @@
package com.gxwebsoft.credit.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditUser;
import com.gxwebsoft.credit.param.CreditUserImportParam;
import com.gxwebsoft.credit.param.CreditUserParam;
@@ -25,7 +23,6 @@ import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -34,7 +31,6 @@ import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
@@ -115,7 +111,7 @@ public class CreditUserController extends BaseController {
@Operation(summary = "删除招投标信息表")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditUserService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditUser.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -148,7 +144,7 @@ public class CreditUserController extends BaseController {
@Operation(summary = "批量删除招投标信息表")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditUserService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditUser.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -181,6 +177,7 @@ public class CreditUserController extends BaseController {
CreditUser::getWinningName,
CreditUser::getCompanyId,
CreditUser::setCompanyId,
CreditUser::setCompanyName,
CreditUser::getHasData,
CreditUser::setHasData,
CreditUser::getTenantId,
@@ -208,19 +205,11 @@ public class CreditUserController extends BaseController {
try {
int sheetIndex = ExcelImportSupport.findSheetIndex(file, "招投标", 0);
List<CreditUserImportParam> list = null;
int usedTitleRows = 0;
int usedHeadRows = 0;
int[][] tryConfigs = new int[][]{{1, 1}, {0, 1}, {0, 2}, {0, 3}};
for (int[] config : tryConfigs) {
list = filterEmptyRows(tryImport(file, config[0], config[1], sheetIndex));
if (!CollectionUtils.isEmpty(list)) {
usedTitleRows = config[0];
usedHeadRows = config[1];
break;
}
}
ExcelImportSupport.ImportResult<CreditUserImportParam> importResult =
ExcelImportSupport.read(file, CreditUserImportParam.class, this::isEmptyImportRow, sheetIndex);
List<CreditUserImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows();
if (CollectionUtils.isEmpty(list)) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
@@ -229,6 +218,11 @@ public class CreditUserController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<Integer, String> urlMap = readNameHyperlinks(file, sheetIndex, usedTitleRows, usedHeadRows);
String fixedCompanyName = null;
if (companyId != null && companyId > 0) {
CreditCompany fixedCompany = creditCompanyService.getById(companyId);
fixedCompanyName = fixedCompany != null ? fixedCompany.getName() : null;
}
final int chunkSize = 500;
final int mpBatchSize = 500;
@@ -246,6 +240,9 @@ public class CreditUserController extends BaseController {
}
if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
@@ -280,35 +277,13 @@ public class CreditUserController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditUserService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditUserService,
chunkItems,
CreditUser::getId,
CreditUser::setId,
CreditUser::getName,
CreditUser::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditUserService.save(rowItem);
if (!saved) {
CreditUser existing = creditUserService.getByName(rowItem.getName());
if (existing != null) {
rowItem.setId(existing.getId());
if (creditUserService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
errorMessages.add("" + rowNumber + "行:保存失败");
return false;
},
mpBatchSize,
CreditUser::getName,
"",
errorMessages
);
chunkItems.clear();
@@ -322,35 +297,13 @@ public class CreditUserController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditUserService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditUserService,
chunkItems,
CreditUser::getId,
CreditUser::setId,
CreditUser::getName,
CreditUser::getName,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditUserService.save(rowItem);
if (!saved) {
CreditUser existing = creditUserService.getByName(rowItem.getName());
if (existing != null) {
rowItem.setId(existing.getId());
if (creditUserService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
errorMessages.add("" + rowNumber + "行:保存失败");
return false;
},
mpBatchSize,
CreditUser::getName,
"",
errorMessages
);
}
@@ -401,15 +354,6 @@ public class CreditUserController extends BaseController {
workbook.close();
}
private List<CreditUserImportParam> tryImport(MultipartFile file, int titleRows, int headRows, int sheetIndex) throws Exception {
ImportParams importParams = new ImportParams();
importParams.setTitleRows(titleRows);
importParams.setHeadRows(headRows);
importParams.setStartSheetIndex(sheetIndex);
importParams.setSheetNum(1);
return ExcelImportUtil.importExcel(file.getInputStream(), CreditUserImportParam.class, importParams);
}
/**
* 读取“项目名称”列的超链接,按数据行顺序返回。
*/
@@ -426,7 +370,7 @@ public class CreditUserController extends BaseController {
if (headerRow != null) {
for (int c = headerRow.getFirstCellNum(); c < headerRow.getLastCellNum(); c++) {
Cell cell = headerRow.getCell(c);
if (cell != null && "项目名称".equals(cell.getStringCellValue())) {
if (cell != null && "项目名称".equals(normalizeHeaderText(cell.getStringCellValue()))) {
nameColIndex = c;
break;
}
@@ -451,14 +395,20 @@ public class CreditUserController extends BaseController {
}
/**
* 过滤掉完全空白的导入行,避免空行导致导入失败
* 用于表头匹配:仅做最常见的空白字符规整(避免表头中存在换行/空格导致无法定位列)。
*/
private List<CreditUserImportParam> filterEmptyRows(List<CreditUserImportParam> rawList) {
if (CollectionUtils.isEmpty(rawList)) {
return rawList;
private static String normalizeHeaderText(String text) {
if (text == null) {
return "";
}
rawList.removeIf(this::isEmptyImportRow);
return rawList;
return text
.replace(" ", "")
.replace("\t", "")
.replace("\r", "")
.replace("\n", "")
.replace("\u00A0", "")
.replace(" ", "")
.trim();
}
private boolean isEmptyImportRow(CreditUserImportParam param) {
@@ -492,6 +442,7 @@ public class CreditUserController extends BaseController {
entity.setRole(param.getRole());
entity.setInfoType(param.getInfoType());
entity.setAddress(param.getAddress());
entity.setPlaintiffAppellant(param.getPlaintiffAppellant());
entity.setProcurementName(param.getProcurementName());
entity.setWinningName(param.getWinningName());
entity.setWinningPrice(param.getWinningPrice());

View File

@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditXgxfController extends BaseController {
@Operation(summary = "删除限制高消费")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditXgxfService.removeById(id)) {
if (batchImportSupport.hardRemoveById(CreditXgxf.class, id)) {
return success("删除成功");
}
return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditXgxfController extends BaseController {
@Operation(summary = "批量删除限制高消费")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditXgxfService.removeByIds(ids)) {
if (batchImportSupport.hardRemoveByIds(CreditXgxf.class, ids)) {
return success("删除成功");
}
return fail("删除失败");
@@ -269,38 +268,13 @@ public class CreditXgxfController extends BaseController {
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditXgxfService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditXgxfService,
chunkItems,
CreditXgxf::getId,
CreditXgxf::setId,
CreditXgxf::getCaseNumber,
CreditXgxf::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditXgxfService.save(rowItem);
if (!saved) {
CreditXgxf existing = creditXgxfService.lambdaQuery()
.eq(CreditXgxf::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditXgxfService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditXgxf::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
@@ -314,38 +288,13 @@ public class CreditXgxfController extends BaseController {
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditXgxfService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey(
creditXgxfService,
chunkItems,
CreditXgxf::getId,
CreditXgxf::setId,
CreditXgxf::getCaseNumber,
CreditXgxf::getCaseNumber,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
boolean saved = creditXgxfService.save(rowItem);
if (!saved) {
CreditXgxf existing = creditXgxfService.lambdaQuery()
.eq(CreditXgxf::getCaseNumber, rowItem.getCaseNumber())
.one();
if (existing != null) {
rowItem.setId(existing.getId());
if (creditXgxfService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditXgxf::getCaseNumber,
"",
errorMessages
);
}
@@ -365,7 +314,7 @@ public class CreditXgxfController extends BaseController {
/**
* 批量导入历史限制高消费(仅解析“历史限制高消费”选项卡)
* 规则:案号相同则覆盖更新recommend++ 记录更新次数);案号不存在则插入。
* 规则:使用数据库唯一索引约束,重复数据不导入。
*/
@PreAuthorize("hasAuthority('credit:creditXgxf:save')")
@Operation(summary = "批量导入历史限制高消费司法大数据")
@@ -398,8 +347,10 @@ public class CreditXgxfController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
LinkedHashMap<String, CreditXgxf> latestByCaseNumber = new LinkedHashMap<>();
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>();
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditXgxf> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) {
CreditXgxfImportParam param = list.get(i);
@@ -437,121 +388,42 @@ public class CreditXgxfController extends BaseController {
// 历史导入的数据统一标记为“失效”
item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item);
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber);
if (item.getRecommend() == null) {
item.setRecommend(0);
}
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistInsertOnlyChunk(
creditXgxfService,
chunkItems,
chunkRowNumbers,
mpBatchSize,
CreditXgxf::getCaseNumber,
"",
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace();
}
}
if (latestByCaseNumber.isEmpty()) {
if (errorMessages.isEmpty()) {
return fail("未读取到数据,请确认模板表头与示例格式一致", null);
}
return success("导入完成成功0条失败" + errorMessages.size() + "", errorMessages);
}
final int chunkSize = 500;
final int mpBatchSize = 500;
List<CreditXgxf> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (Map.Entry<String, CreditXgxf> entry : latestByCaseNumber.entrySet()) {
String caseNumber = entry.getKey();
CreditXgxf item = entry.getValue();
Integer rowNo = latestRowByCaseNumber.get(caseNumber);
chunkItems.add(item);
chunkRowNumbers.add(rowNo != null ? rowNo : -1);
if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback(
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditXgxfService,
chunkItems,
CreditXgxf::getId,
CreditXgxf::setId,
CreditXgxf::getCaseNumber,
CreditXgxf::getCaseNumber,
CreditXgxf::getRecommend,
CreditXgxf::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditXgxfService.save(rowItem);
if (!saved) {
CreditXgxf existing = creditXgxfService.lambdaQuery()
.eq(CreditXgxf::getCaseNumber, rowItem.getCaseNumber())
.select(CreditXgxf::getId, CreditXgxf::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditXgxfService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
errorMessages
);
chunkItems.clear();
chunkRowNumbers.clear();
}
}
if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback(
successCount += batchImportSupport.persistInsertOnlyChunk(
creditXgxfService,
chunkItems,
chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate(
creditXgxfService,
chunkItems,
CreditXgxf::getId,
CreditXgxf::setId,
CreditXgxf::getCaseNumber,
CreditXgxf::getCaseNumber,
CreditXgxf::getRecommend,
CreditXgxf::setRecommend,
null,
mpBatchSize
),
(rowItem, rowNumber) -> {
if (rowItem.getRecommend() == null) {
rowItem.setRecommend(0);
}
boolean saved = creditXgxfService.save(rowItem);
if (!saved) {
CreditXgxf existing = creditXgxfService.lambdaQuery()
.eq(CreditXgxf::getCaseNumber, rowItem.getCaseNumber())
.select(CreditXgxf::getId, CreditXgxf::getRecommend)
.one();
if (existing != null) {
rowItem.setId(existing.getId());
Integer old = existing.getRecommend();
rowItem.setRecommend(old == null ? 1 : old + 1);
if (creditXgxfService.updateById(rowItem)) {
return true;
}
}
} else {
return true;
}
String prefix = rowNumber > 0 ? ("" + rowNumber + "行:") : "";
errorMessages.add(prefix + "保存失败");
return false;
},
mpBatchSize,
CreditXgxf::getCaseNumber,
"",
errorMessages
);
}
@@ -606,20 +478,29 @@ public class CreditXgxfController extends BaseController {
private CreditXgxf convertImportParamToEntity(CreditXgxfImportParam param) {
CreditXgxf entity = new CreditXgxf();
// Template compatibility: some upstream multi-company exports use alternate headers for the same columns.
String plaintiffAppellant = !ImportHelper.isBlank(param.getPlaintiffAppellant())
? param.getPlaintiffAppellant()
: param.getPlaintiffAppellant2();
String appellee = !ImportHelper.isBlank(param.getAppellee())
? param.getAppellee()
: param.getDataType();
String courtName = !ImportHelper.isBlank(param.getCourtName())
? param.getCourtName()
: param.getCourtName2();
entity.setCaseNumber(param.getCaseNumber());
entity.setType(param.getType());
entity.setDataType(param.getDataType());
entity.setPlaintiffAppellant(param.getPlaintiffAppellant());
entity.setAppellee(param.getAppellee());
entity.setPlaintiffAppellant(plaintiffAppellant);
entity.setAppellee(appellee);
entity.setOtherPartiesThirdParty(param.getOtherPartiesThirdParty());
entity.setPlaintiffAppellant(param.getPlaintiffAppellant());
entity.setDataStatus(param.getDataStatus());
entity.setAppellee(param.getAppellee());
// 兼容不同模板字段:如果 *2 有值则以 *2 为准写入主字段
entity.setInvolvedAmount(param.getInvolvedAmount());
entity.setOccurrenceTime(param.getOccurrenceTime());
entity.setCourtName(param.getCourtName());
entity.setCourtName(courtName);
entity.setReleaseDate(param.getReleaseDate());
entity.setComments(param.getComments());

View File

@@ -2,8 +2,10 @@ package com.gxwebsoft.credit.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import com.gxwebsoft.credit.excel.ExcelHeaderAlias;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataFormatter;
@@ -18,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -277,7 +280,7 @@ public class ExcelImportSupport {
if (workbook.getNumberOfSheets() > sheetIndex) {
Sheet sheet = workbook.getSheetAt(sheetIndex);
if (sheet != null) {
normalizeHeaderCells(sheet, titleRows, headRows);
normalizeHeaderCells(sheet, titleRows, headRows, clazz);
}
}
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
@@ -289,10 +292,14 @@ public class ExcelImportSupport {
}
}
private static void normalizeHeaderCells(Sheet sheet, int titleRows, int headRows) {
private static void normalizeHeaderCells(Sheet sheet, int titleRows, int headRows, Class<?> clazz) {
if (sheet == null || headRows <= 0) {
return;
}
Map<String, String> expectedHeadersByKey = buildExpectedHeaderKeyMap(clazz);
if (expectedHeadersByKey.isEmpty()) {
return;
}
int headerStart = Math.max(titleRows, 0);
int headerEnd = headerStart + headRows - 1;
for (int r = headerStart; r <= headerEnd; r++) {
@@ -317,21 +324,99 @@ public class ExcelImportSupport {
continue;
}
String normalized = normalizeHeaderText(text);
if (normalized != null && !normalized.equals(text)) {
cell.setCellValue(normalized);
String canonical = findCanonicalHeader(text, expectedHeadersByKey);
if (canonical != null && !canonical.equals(text)) {
cell.setCellValue(canonical);
}
}
}
}
private static String normalizeHeaderText(String text) {
if (text == null) {
private static Map<String, String> buildExpectedHeaderKeyMap(Class<?> clazz) {
if (clazz == null) {
return Collections.emptyMap();
}
Map<String, String> map = new HashMap<>();
Class<?> current = clazz;
while (current != null && current != Object.class) {
Field[] fields = current.getDeclaredFields();
for (Field field : fields) {
Excel excel = field.getAnnotation(Excel.class);
if (excel == null) {
continue;
}
String name = excel.name();
if (name == null || name.trim().isEmpty()) {
continue;
}
String key = normalizeHeaderKey(name);
if (!key.isEmpty()) {
// key -> canonical annotation name
map.putIfAbsent(key, name);
}
// Allow import-time header aliases without changing the exported template header.
ExcelHeaderAlias alias = field.getAnnotation(ExcelHeaderAlias.class);
if (alias != null && alias.value() != null) {
for (String aliasName : alias.value()) {
if (aliasName == null || aliasName.trim().isEmpty()) {
continue;
}
String aliasKey = normalizeHeaderKey(aliasName);
if (!aliasKey.isEmpty()) {
map.putIfAbsent(aliasKey, name);
}
}
}
}
current = current.getSuperclass();
}
return map;
}
private static String findCanonicalHeader(String rawHeaderText, Map<String, String> expectedHeadersByKey) {
if (rawHeaderText == null) {
return null;
}
// Remove common invisible whitespace characters, including full-width space.
return text
String key = normalizeHeaderKey(rawHeaderText);
if (key.isEmpty()) {
return null;
}
String canonical = expectedHeadersByKey.get(key);
if (canonical != null) {
return canonical;
}
// Some upstream templates append explanations in brackets, e.g. "原告/上诉人(申请执行人)".
// Only strip trailing bracket groups when the full header doesn't match any known expected header.
String strippedKey = stripTrailingBracketSuffix(key);
if (!strippedKey.equals(key)) {
canonical = expectedHeadersByKey.get(strippedKey);
if (canonical != null) {
return canonical;
}
}
return null;
}
/**
* Normalize header text for matching purposes only.
*
* <p>Do NOT drop bracket content (e.g. keep "(元)" or "(公告)" in the middle), because many templates use them as
* part of the canonical header name. We only remove whitespace and unify common full-width punctuation.</p>
*/
private static String normalizeHeaderKey(String text) {
if (text == null) {
return "";
}
String normalized = text
// unify common full-width punctuation
.replace("", "/")
.replace("", "[")
.replace("", "]")
// remove common invisible whitespace characters, including full-width space.
.replace(" ", "")
.replace("\t", "")
.replace("\r", "")
@@ -339,6 +424,34 @@ public class ExcelImportSupport {
.replace("\u00A0", "")
.replace(" ", "")
.trim();
// Make ( ) and comparable by normalizing both to ASCII.
normalized = normalized.replace('', '(').replace('', ')');
return normalized;
}
private static String stripTrailingBracketSuffix(String text) {
if (text == null) {
return "";
}
String s = text.trim();
while (true) {
if (s.endsWith(")")) {
int idx = s.lastIndexOf('(');
if (idx >= 0) {
s = s.substring(0, idx).trim();
continue;
}
}
if (s.endsWith("]")) {
int idx = s.lastIndexOf('[');
if (idx >= 0) {
s = s.substring(0, idx).trim();
continue;
}
}
return s;
}
}
private static <T> List<T> filterEmptyRows(List<T> rawList, Predicate<T> emptyRowPredicate) {
@@ -553,6 +666,10 @@ public class ExcelImportSupport {
}
private static int findColumnIndexByHeader(Sheet sheet, int titleRows, int headRows, String headerName) {
String targetKey = normalizeHeaderKey(headerName);
if (targetKey.isEmpty()) {
return -1;
}
int firstHeaderRow = Math.max(0, titleRows);
int lastHeaderRow = Math.max(0, titleRows + headRows - 1);
for (int r = firstHeaderRow; r <= lastHeaderRow; r++) {
@@ -565,17 +682,10 @@ public class ExcelImportSupport {
if (cell == null) {
continue;
}
if (cell.getCellType() == CellType.STRING) {
String value = cell.getStringCellValue();
if (headerName.equals(value != null ? value.trim() : null)) {
return c;
}
} else {
DataFormatter formatter = new DataFormatter();
String value = formatter.formatCellValue(cell);
if (headerName.equals(value != null ? value.trim() : null)) {
return c;
}
DataFormatter formatter = new DataFormatter();
String value = formatter.formatCellValue(cell);
if (targetKey.equals(normalizeHeaderKey(value))) {
return c;
}
}
}

View File

@@ -71,6 +71,9 @@ public class CreditAdministrativeLicense implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "主体企业")
@TableField(exist = false)
private String companyName;

View File

@@ -55,6 +55,9 @@ public class CreditBranch implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "主题企业")
@TableField(exist = false)
private String companyName;

View File

@@ -176,6 +176,9 @@ public class CreditCompany implements Serializable {
@Schema(description = "类型")
private Integer type;
@Schema(description = "是否客户")
private Integer isCustomer;
@Schema(description = "上级id, 0是顶级")
private Integer parentId;

View File

@@ -57,8 +57,10 @@ public class CreditCompetitor implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "企业名称")
@TableField(exist = false)
private String companyName;
@Schema(description = "所属企业名称")

View File

@@ -42,7 +42,7 @@ public class CreditCourtAnnouncement implements Serializable {
@Schema(description = "公告类型")
private String dataType;
@Schema(description = "公告")
@Schema(description = "原告/上诉")
private String plaintiffAppellant;
@Schema(description = "刊登日期")

View File

@@ -51,8 +51,10 @@ public class CreditCustomer implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "企业名称")
@TableField(exist = false)
private String companyName;
@Schema(description = "是否有数据")

View File

@@ -78,8 +78,10 @@ public class CreditExternal implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "企业名称")
@TableField(exist = false)
private String companyName;
@Schema(description = "是否有数据")

View File

@@ -49,6 +49,9 @@ public class CreditHistoricalLegalPerson implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "主体企业")
@TableField(exist = false)
private String companyName;

View File

@@ -61,6 +61,9 @@ public class CreditJudgmentDebtor implements Serializable {
@Schema(description = "涉案金额")
private String amount;
@Schema(description = "执行标的(元)")
private String involvedAmountQcc;
@Schema(description = "法院")
private String courtName;

View File

@@ -34,7 +34,7 @@ public class CreditJudicialDocument implements Serializable {
private String title;
@Schema(description = "文书类型")
private String type;
private String documentType;
@Schema(description = "案号")
private String caseNumber;
@@ -48,7 +48,7 @@ public class CreditJudicialDocument implements Serializable {
@Schema(description = "当事人")
private String otherPartiesThirdParty;
@Schema(description = "案件金额(元)")
@Schema(description = "案件金额")
private String involvedAmount;
@Schema(description = "裁判结果")

View File

@@ -0,0 +1,268 @@
package com.gxwebsoft.credit.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableLogic;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 小程序端客户
*
* @author 科技小王子
* @since 2026-03-16 20:59:17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "CreditMpCustomer对象", description = "小程序端客户")
public class CreditMpCustomer implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "ID")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "拖欠方")
private String toUser;
@Schema(description = "拖欠金额")
private String price;
@Schema(description = "拖欠年数")
private String years;
@Schema(description = "链接")
private String url;
@Schema(description = "状态")
private String statusTxt;
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "所在省份")
private String province;
@Schema(description = "所在城市")
private String city;
@Schema(description = "所在辖区")
private String region;
@Schema(description = "文件路径")
private String files;
@Schema(description = "是否有数据")
private Boolean hasData;
@Schema(description = "步骤, 0未受理1已受理2材料提交3合同签订4执行回款5完结")
private Integer step;
@Schema(description = "备注")
private String comments;
@Schema(description = "是否推荐")
private Integer recommend;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "状态, 0未受理, 1已受理")
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "用户昵称")
@TableField(exist = false)
private String nickname;
@Schema(description = "用户手机号")
@TableField(exist = false)
private String phone;
@Schema(description = "用户头像")
@TableField(exist = false)
private String avatar;
@Schema(description = "真实姓名")
@TableField(exist = false)
private String realName;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
// 第1步案件受理字段
@Schema(description = "第1步是否已提交")
private Integer followStep1Submitted;
@Schema(description = "第1步提交时间")
private String followStep1SubmittedAt;
@Schema(description = "第1步是否需要审核")
private Integer followStep1NeedApproval;
@Schema(description = "第1步是否审核通过")
private Integer followStep1Approved;
@Schema(description = "第1步审核时间")
private String followStep1ApprovedAt;
@Schema(description = "第1步审核人ID")
private Long followStep1ApprovedBy;
// 第2步材料准备字段
@Schema(description = "第2步是否已提交")
private Integer followStep2Submitted;
@Schema(description = "第2步提交时间")
private String followStep2SubmittedAt;
@Schema(description = "第2步是否需要审核")
private Integer followStep2NeedApproval;
@Schema(description = "第2步是否审核通过")
private Integer followStep2Approved;
@Schema(description = "第2步审核时间")
private String followStep2ApprovedAt;
@Schema(description = "第2步审核人ID")
private Long followStep2ApprovedBy;
// 第3步案件办理字段
@Schema(description = "第3步是否已提交")
private Integer followStep3Submitted;
@Schema(description = "第3步提交时间")
private String followStep3SubmittedAt;
@Schema(description = "第3步是否需要审核")
private Integer followStep3NeedApproval;
@Schema(description = "第3步是否审核通过")
private Integer followStep3Approved;
@Schema(description = "第3步审核时间")
private String followStep3ApprovedAt;
@Schema(description = "第3步审核人ID")
private Long followStep3ApprovedBy;
// 第4步送达签收字段
@Schema(description = "第4步是否已提交")
private Integer followStep4Submitted;
@Schema(description = "第4步提交时间")
private String followStep4SubmittedAt;
@Schema(description = "第4步是否需要审核")
private Integer followStep4NeedApproval;
@Schema(description = "第4步是否审核通过")
private Integer followStep4Approved;
@Schema(description = "第4步审核时间")
private String followStep4ApprovedAt;
@Schema(description = "第4步审核人ID")
private Long followStep4ApprovedBy;
// 第5步合同签订字段
@Schema(description = "第5步是否已提交")
private Integer followStep5Submitted;
@Schema(description = "第5步提交时间")
private String followStep5SubmittedAt;
@Schema(description = "第5步合同信息JSON数组")
private String followStep5Contracts;
@Schema(description = "第5步是否需要审核")
private Integer followStep5NeedApproval;
@Schema(description = "第5步是否审核通过")
private Integer followStep5Approved;
@Schema(description = "第5步审核时间")
private String followStep5ApprovedAt;
@Schema(description = "第5步审核人ID")
private Long followStep5ApprovedBy;
// 第6步订单回款字段
@Schema(description = "第6步是否已提交")
private Integer followStep6Submitted;
@Schema(description = "第6步提交时间")
private String followStep6SubmittedAt;
@Schema(description = "第6步财务录入的回款记录JSON数组")
private String followStep6PaymentRecords;
@Schema(description = "第6步预计回款JSON数组")
private String followStep6ExpectedPayments;
@Schema(description = "第6步是否需要审核")
private Integer followStep6NeedApproval;
@Schema(description = "第6步是否审核通过")
private Integer followStep6Approved;
@Schema(description = "第6步审核时间")
private String followStep6ApprovedAt;
@Schema(description = "第6步审核人ID")
private Long followStep6ApprovedBy;
// 第7步电话回访字段
@Schema(description = "第7步是否已提交")
private Integer followStep7Submitted;
@Schema(description = "第7步提交时间")
private String followStep7SubmittedAt;
@Schema(description = "第7步回访记录JSON数组")
private String followStep7VisitRecords;
@Schema(description = "第7步是否需要审核")
private Integer followStep7NeedApproval;
@Schema(description = "第7步是否审核通过")
private Integer followStep7Approved;
@Schema(description = "第7步审核时间")
private String followStep7ApprovedAt;
@Schema(description = "第7步审核人ID")
private Long followStep7ApprovedBy;
// 流程结束字段
@Schema(description = "流程是否已结束")
private Integer followProcessEnded;
@Schema(description = "流程结束时间")
private String followProcessEndTime;
@Schema(description = "流程结束原因")
private String followProcessEndReason;
}

View File

@@ -61,7 +61,7 @@ public class CreditNearbyCompany implements Serializable {
@Schema(description = "邮箱")
private String email;
@Schema(description = "邮箱")
@Schema(description = "更多邮箱")
private String moreEmail;
@Schema(description = "所在国家")
@@ -79,6 +79,9 @@ public class CreditNearbyCompany implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "主体企业")
@TableField(exist = false)
private String companyName;
@@ -131,7 +134,7 @@ public class CreditNearbyCompany implements Serializable {
@Schema(description = "通信地址")
private String mailingAddress;
@Schema(description = "通信地址邮")
@Schema(description = "通信地址邮")
private String mailingEmail;
@Schema(description = "企业简介")
@@ -164,35 +167,45 @@ public class CreditNearbyCompany implements Serializable {
@Schema(description = "上级id, 0是顶级")
private Integer parentId;
@Schema(description = "实缴资本")
private String paidinCapital;
@Schema(description = "登记机关")
private String registrationAuthority;
@Schema(description = "纳税人资质")
private String taxpayerQualification;
@Schema(description = "最新年报年份")
private String latestAnnualReportYear;
@Schema(description = "最新年报营业收入")
private String latestAnnualReportOnOperatingRevenue;
@Schema(description = "企查分")
private String enterpriseScoreCheck;
@Schema(description = "信用等级")
private String creditRating;
@Schema(description = "科创分")
private String cechnologyScore;
@Schema(description = "科创等级")
private String cechnologyLevel;
@Schema(description = "是否小微企业")
private String smallEnterprise;
// @Schema(description = "实缴资本")
// @TableField(exist = false)
// private String paidinCapital;
//
// @Schema(description = "登记机关")
// @TableField(exist = false)
// private String registrationAuthority;
//
// @Schema(description = "纳税人资质")
// @TableField(exist = false)
// private String taxpayerQualification;
//
// @Schema(description = "最新年报年份")
// @TableField(exist = false)
// private String latestAnnualReportYear;
//
// @Schema(description = "最新年报营业收入")
// @TableField(exist = false)
// private String latestAnnualReportOnOperatingRevenue;
//
// @Schema(description = "企查分")
// @TableField(exist = false)
// private String enterpriseScoreCheck;
//
// @Schema(description = "信用等级")
// @TableField(exist = false)
// private String creditRating;
//
// @Schema(description = "科创分")
// @TableField(exist = false)
// private String cechnologyScore;
//
// @Schema(description = "科创等级")
// @TableField(exist = false)
// private String cechnologyLevel;
//
// @Schema(description = "是否小微企业")
// @TableField(exist = false)
// private String smallEnterprise;
@Schema(description = "是否有数据")
private Boolean hasData;

View File

@@ -50,8 +50,10 @@ public class CreditRiskRelation implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "企业名称")
@TableField(exist = false)
private String companyName;
@Schema(description = "是否有数据")

View File

@@ -51,8 +51,10 @@ public class CreditSupplier implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "企业名称")
@TableField(exist = false)
private String companyName;
@Schema(description = "是否有数据")

View File

@@ -64,6 +64,9 @@ public class CreditSuspectedRelationship implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "主体企业")
@TableField(exist = false)
private String companyName;

View File

@@ -39,6 +39,9 @@ public class CreditUser implements Serializable {
@Schema(description = "类型, 0普通用户, 1招投标")
private Integer type;
@Schema(description = "原告/上诉人")
private String plaintiffAppellant;
@Schema(description = "企业角色")
private String role;
@@ -78,8 +81,10 @@ public class CreditUser implements Serializable {
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "企业别名")
private String companyAlias;
@Schema(description = "企业名称")
@TableField(exist = false)
private String companyName;
@Schema(description = "是否有数据")

View File

@@ -30,51 +30,51 @@ public class CreditXgxf implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "案号")
private String caseNumber;
@Schema(description = "链接地址")
private String url;
@Schema(description = "数据类型")
private String type;
@Schema(description = "限消令对象")
private String dataType;
@Schema(description = "限制法定代表人")
@Schema(description = "原告/上诉人")
private String plaintiffAppellant;
@Schema(description = "申请人")
private String appellee;
@Schema(description = "原告/上诉人")
private String plaintiffUser;
@Schema(description = "被告/被上诉人")
private String defendantUser;
@Schema(description = "涉案金额(元)")
private String involvedAmount;
@Schema(description = "立案日期")
private String occurrenceTime;
@Schema(description = "执行法院")
private String courtName;
@Schema(description = "发布日期")
private String releaseDate;
private String appellee;
@Schema(description = "其他当事人/第三人")
private String otherPartiesThirdParty;
@Schema(description = "案由")
private String causeOfAction;
@Schema(description = "发生时间")
private String occurrenceTime;
@Schema(description = "案号")
private String caseNumber;
@Schema(description = "涉案金额")
private String involvedAmount;
@Schema(description = "法院")
private String courtName;
@Schema(description = "数据状态")
private String dataStatus;
@Schema(description = "限消令对象")
private String dataType;
@Schema(description = "原告/上诉人2")
private String plaintiffUser;
@Schema(description = "被告/被上诉人2")
private String defendantUser;
@Schema(description = "发布日期")
private String releaseDate;
@Schema(description = "案由")
private String causeOfAction;
@Schema(description = "企业ID")
private Integer companyId;

View File

@@ -0,0 +1,23 @@
package com.gxwebsoft.credit.excel;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Excel 导入表头别名。
*
* <p>EasyPOI 的 {@code @Excel(name=...)} 仅支持一个表头名;当上游模板存在多种表头写法时,
* 可用此注解声明别名,让 {@link com.gxwebsoft.credit.controller.ExcelImportSupport} 在导入前把别名规范化为 canonical 表头。</p>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelHeaderAlias {
/**
* 允许匹配的表头别名列表(任意一个匹配即视为该列)。
*/
String[] value();
}

View File

@@ -0,0 +1,47 @@
package com.gxwebsoft.credit.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.credit.entity.CreditMpCustomer;
import com.gxwebsoft.credit.param.CreditMpCustomerParam;
import com.gxwebsoft.credit.param.FollowStepQueryDTO;
import com.gxwebsoft.credit.vo.PendingApprovalStepVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 小程序端客户Mapper
*
* @author 科技小王子
* @since 2026-03-16 20:59:17
*/
public interface CreditMpCustomerMapper extends BaseMapper<CreditMpCustomer> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<CreditMpCustomer>
*/
List<CreditMpCustomer> selectPageRel(@Param("page") IPage<CreditMpCustomer> page,
@Param("param") CreditMpCustomerParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<CreditMpCustomer> selectListRel(@Param("param") CreditMpCustomerParam param);
/**
* 获取待审核的跟进步骤列表
*
* @param query 查询参数
* @return 待审核步骤列表
*/
List<PendingApprovalStepVO> selectPendingApprovalSteps(@Param("param") FollowStepQueryDTO query);
}

Some files were not shown because too many files have changed in this diff Show More