Compare commits

..

157 Commits

Author SHA1 Message Date
120082241e feat(customer): 添加客户跟进步骤字段和审批逻辑
- 在CreditMpCustomer实体中新增第1步跟进相关字段,包括联系人、电话、录音、截图等信息
- 实现服务层中对第1至第4步审批状态的设置逻辑
- 添加审批时间、审批人等审计信息的存储功能
- 支持多步骤客户跟进流程的数据管理需求
2026-03-23 17:25:32 +08:00
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
e8ce2d162f feat(ticket): 优化套票发放逻辑支持购买量与赠送量分离处理
- 引入 GltTicketOrder 实体和服务类处理送水订单
- 新增 DateTimeFormatter 用于时间格式化
- 修改购买数量计算逻辑,优先使用订单总数量提高准确性
- 实现购买量与赠送量分离,支持 includeBuyQty 配置决定是否将购买量计入水票账户
- 更新用户水票的可用量、冻结量和已释放数量字段逻辑
- 优化起始送水功能,支持从可用和冻结票中同时扣除
- 添加订单总数量与订单商品数量不一致的提示日志
- 在起始送水时自动生成对应的送水订单记录
- 调整释放计划生成逻辑,基于实际冻结量进行计算
2026-02-09 18:09:24 +08:00
aa4a6d9725 feat(ticket): 添加套票模板起始送水自动核销功能
- 在套票发放时根据模板配置的startSendQty自动核销对应数量
- 新增CHANGE_TYPE_START_SEND_WRITE_OFF变更类型用于标识起始送水自动核销
- 实现自动核销逻辑:计算可用数量并更新用户套票的可用和已用数量
- 记录自动核销日志并关联原始商城订单便于追溯核销来源
- 更新套票发放任务注释说明自动核销功能应用场景
2026-02-09 17:35:21 +08:00
f7a96724c6 feat(task): 更新分销佣金解冻和套票发放任务逻辑
- 引入GltTicketTemplate和GltTicketTemplateService用于动态获取水票模板
- 将硬编码的formId=10074替换为从水票模板表动态获取的goodsId集合
- 修改经销商佣金解冻规则适配新的模板配置方式
- 更新套票发放任务支持多商品ID配置
- 添加水票模板数据加载和验证逻辑
- 增强任务执行前的模板配置检查机制
2026-02-09 17:30:43 +08:00
3d8169c55a fix(auth): 修正水票模板权限注解错误
- 将添加水票接口的权限从 save 修改为 update
- 将批量添加水票接口的权限从 save 修改为 update
- 确保权限控制与实际操作类型匹配
2026-02-09 16:22:03 +08:00
3b4f8a29d8 feat(order): 添加配送范围电子围栏校验功能
- 在订单创建流程中集成电子围栏校验机制
- 实现不信任前端坐标的地址表坐标验证策略
- 添加多种格式的围栏points解析支持(JSON、分号分隔等)
- 实现射线投射算法进行点在多边形内判断
- 添加自提和无需物流订单的围栏校验跳过逻辑
- 实现坐标缺失和异常情况的错误处理机制
- 添加围栏配置异常时的订单拒绝保护机制
- 创建GeoFenceUtil工具类提供完整的围栏功能支持
2026-02-09 11:16:04 +08:00
efe7904755 fix(database): 修复导航查询中的数据库连接条件问题
- 为父级导航连接添加删除状态和租户ID过滤条件
- 移除模型连接中不必要的 c.model = 0 条件以避免字符串转数字比较
- 为模型连接添加删除状态和租户ID过滤条件
- 添加注释说明原条件导致的性能问题
2026-02-09 10:08:46 +08:00
01cd94e8b0 fix(database): 修复导航查询条件和信用信息搜索功能
- 修正了CmsNavigationMapper中cms_model表关联查询条件,添加model=0的过滤条件
- 扩展了CreditXgxfMapper中的关键词搜索范围,增加原告、被告、法院名称和其他当事人字段的搜索支持
- 优化了数据库查询逻辑以提高搜索准确性和性能
2026-02-09 10:01:20 +08:00
15744e668b feat(controller): 批量导入支持新增企业查询条件定制功能
- 在 BatchImportSupport 中添加了新的重载方法 refreshCompanyIdByCompanyNameContainedInText
- 新增 companyQueryCustomizer 参数用于自定义 CreditCompany 查询条件
- 在 CreditJudgmentDebtorController 中添加 topLevelOnly 参数控制是否只匹配一级企业
- 支持通过 parentId 条件过滤一级企业(parentId=0 或 NULL)
- 优化了企业名称匹配逻辑,专注于 name 字段进行匹配
2026-02-08 13:15:58 +08:00
7841fa0bba feat(credit): 增加企查查历史被执行人数据导入兼容性
- 添加涉案金额字段用于匹配企查查表头
- 新增执行标的金额兼容字段involvedAmountQcc
- 新增执行法院兼容字段courtNameQcc
- 实现金额和法院名称的多源数据映射逻辑
- 添加公司ID追踪功能用于数据关联
- 优化导入时的空值检查和数据清理
2026-02-08 02:01:01 +08:00
051abb9d7a feat(shop): 新增电子围栏功能并重构仓库模块
- 添加 ShopStoreFence 实体类及相关数据库表映射
- 实现 ShopStoreFenceController 提供完整的 CRUD 操作接口
- 创建 ShopStoreFenceService 和 ShopStoreFenceServiceImpl 业务逻辑层
- 设计 ShopStoreFenceParam 查询参数类支持条件筛选
- 新建 ShopStoreFenceMapper 及其 XML 映射文件
- 将原有的 ShopWarehouse 重命名为 ShopStoreWarehouse 并更新相关引用
- 修改 GltTicketOrderMapper 和 ShopOrderMapper 中的仓库表关联关系
- 更新 ShopWarehouse 相关的所有控制器、服务、参数和映射文件命名
- 在订单相关查询中将 shop_warehouse 表替换为 shop_store_warehouse 表
- 为仓库控制器添加用户登录信息自动填充功能
2026-02-08 00:03:20 +08:00
05a94b29b5 feat(shop): 新增电子围栏功能并重构仓库模块
- 添加 ShopStoreFence 实体类及相关数据库表映射
- 实现 ShopStoreFenceController 提供完整的 CRUD 操作接口
- 创建 ShopStoreFenceService 和 ShopStoreFenceServiceImpl 业务逻辑层
- 设计 ShopStoreFenceParam 查询参数类支持条件筛选
- 新建 ShopStoreFenceMapper 及其 XML 映射文件
- 将原有的 ShopWarehouse 重命名为 ShopStoreWarehouse 并更新相关引用
- 修改 GltTicketOrderMapper 和 ShopOrderMapper 中的仓库表关联关系
- 更新 ShopWarehouse 相关的所有控制器、服务、参数和映射文件命名
- 在订单相关查询中将 shop_warehouse 表替换为 shop_store_warehouse 表
- 为仓库控制器添加用户登录信息自动填充功能
2026-02-07 18:51:35 +08:00
3e2b48ace4 feat(entity): 添加用户地址修改时间字段
- 在 ShopUserAddress 实体中新增 updateTime 字段
- 为 updateTime 字段添加 JsonFormat 注解支持格式化输出
- 为 updateTime 字段添加 Schema 注解提供接口文档描述
2026-02-07 18:04:00 +08:00
45878b9005 feat(glt): 实现送水订单配送员提成结算功能
- 修改经销商订单结算任务,按确认收货状态结算订单(deliveryStatus=20)
- 在送水订单控制器中添加配送员提成结算注释说明
- 扩展送水订单服务接口,新增超时自动确认收货方法
- 实现送水订单配送员提成结算逻辑,支持拍照上传和用户确认收货两种触发方式
- 添加配送员提成幂等处理,避免重复入账
- 创建租户10584送水订单超时自动确认收货定时任务
- 实现超时订单自动确认收货并触发配送员提成结算功能
2026-02-07 17:56:32 +08:00
c0c1232768 feat(shop): 新增分销佣金解冻功能并扩展资金流动类型
- 在 ShopDealerCapital 和 ShopDealerCapitalParam 中添加资金流动类型 50(佣金解冻)
- 新增 DealerCommissionUnfreeze10584Task 定时任务处理分销佣金解冻逻辑
- 实现送水套餐和非送水套餐的差异化解冻规则
- 添加基于订单状态和水票配送状态的解冻条件判断
- 实现幂等性检查防止重复解冻操作
- 添加分布式锁确保并发安全的解冻处理
- 记录解冻流水作为佣金解冻的标记凭证
2026-02-07 17:26:19 +08:00
54e2654033 ```
feat(settlement): 修改佣金计算逻辑并计入分销商冻结金额

- 将佣金发放逻辑修改为先计入 ShopDealerUser.freezeMoney 而不是直接入账
- 更新任务描述文档,明确佣金先计入冻结金额的流程
- 在 findUnsettledPaidOrders 方法中添加相关注释说明
- 修改日志信息从"佣金入账"为"佣金入冻结"
- 调整 SQL 更新语句将 money 字段改为 freeze_money 字段
- 添加防止并发丢失更新的安全机制注释
```
2026-02-07 17:01:46 +08:00
28f113a6c9 feat(withdraw): 添加提现自动审核逻辑和订单过期时间调整
- 为租户ID 10584添加提现自动审核功能,金额小于等于50元自动通过
- 设置提现金额大于50小于1000需人工审核
- 调整订单过期时间从10分钟延长至60分钟
2026-02-07 16:37:43 +08:00
d50e85fc52 fix(order): 修复订单取消和退款流程中的并发安全问题
- 添加订单ID空值检查,防止空指针异常
- 使用条件更新替代直接更新,避免并发导致的状态污染
- 扩展退款状态检查范围,包含更多退款相关状态
- 添加未支付订单退款验证,防止脏状态产生
- 增加重复退款申请检查,避免状态冲突
- 区分用户取消和系统取消的原因标记
- 优化更新逻辑确保状态一致性
2026-02-07 15:36:35 +08:00
9b31b3ce57 feat(payment): 优化微信支付功能并添加回调兼容性支持
- 修复信用风险关系查询中的公司ID条件判断逻辑
- 将关键词搜索从main_body_name改为match_name以提高匹配准确性
- 在安全配置中添加/api/system/wx-pay/**路径到公共访问白名单
- 添加RoundingMode导入和WechatPrepaySnapshot数据类用于支付快照管理
- 实现微信支付预下单快照机制以解决请求重入参数不一致问题
- 添加多种回调地址候选方案包括新默认地址和历史兼容地址
- 实现支付金额转换为分的统一方法toFen,使用四舍五入避免精度问题
- 添加Redis缓存存储支付快照,TTL设置为30分钟
- 实现notifyUrl动态切换重试机制,支持多个回调地址备选
- 创建WxPayNotifyAliasController提供旧版回调地址兼容性支持
- 修复JSAPI和Native支付中的金额计算逻辑,优先使用payPrice字段
- 添加支付金额空值检查防止运行时异常
- 优化支付描述字段截断处理逻辑,改进默认值设置
2026-02-07 14:01:04 +08:00
78a3f8ce4c feat(order): 添加订单创建时间和过期时间设置功能
- 引入 LocalDateTime 用于时间操作
- 在订单创建时自动设置 createTime 和 updateTime
- 设置订单默认10分钟过期时间用于支付校验
- 确保时间戳只在为空时进行初始化避免覆盖
- 统一在服务层和控制层实现时间设置逻辑
2026-02-07 13:12:11 +08:00
fc8d49a768 feat(GltTicketTemplate): 添加根据商品ID查询水票功能
- 引入LambdaQueryWrapper用于构建查询条件
- 实现getByGoodsId接口支持按商品ID查询水票模板
- 添加按商品ID、删除状态排序和创建时间降序排序逻辑
- 限制查询结果只返回最新的一条记录
- 集成到现有服务层调用体系中
2026-02-07 12:39:58 +08:00
a20d1dd465 feat(tickets): 实现冻结水票自动释放功能
- 在 GltUserTicketMapper 中新增 releaseFrozenQty 方法用于释放冻结水票
- 新增 GltUserTicketReleaseMapper 处理释放记录的查询和状态更新
- 添加 H2 数据库依赖支持单元测试
- 创建 GltUserTicketAutoReleaseService 接口及其实现类
- 实现自动释放服务的核心逻辑包括加锁查询、数量更新和流水记录
- 新建定时任务 GltUserTicketAutoReleaseTask 定期执行释放操作
- 添加完整的单元测试覆盖正常释放、未到期和冻结不足等场景
- 创建测试专用数据库表结构文件
2026-02-07 00:25:24 +08:00
46de27611d fix(database): 修正水票配送订单表名错误
- 将 ALTER TABLE 语句中的表名从 glt_ticket_order 修正为 glt_ticket_order2
- 确保配送流程相关字段正确添加到目标表结构中
2026-02-06 23:31:50 +08:00
1d63c5cdc8 fix(controller): 修复登录验证失败返回值问题
- 修改riderPage方法中登录验证失败时的返回值格式
- 统一fail方法调用参数结构
2026-02-06 23:18:26 +08:00
922e7def9d feat(ticket): 添加水票订单配送流程功能
- 在GltTicketOrder实体类中新增配送相关的字段,包括配送状态、配送时间、收货人信息等
- 实现配送员端订单查询接口,支持按配送状态筛选和权限隔离
- 添加配送流程核心接口:接单、开始配送、确认送达、用户确认收货等功能
- 实现配送状态流转的状态机校验和并发安全的原子更新操作
- 优化数据库查询SQL,增加配送状态和租户ID的索引提升查询性能
- 添加配送员身份验证和权限检查机制,确保操作安全性
2026-02-06 22:00:59 +08:00
7191c93b4c feat(shop): 添加仓库关联功能到店铺实体
- 在 ShopStore 实体中添加 warehouseId 和 warehouseName 字段
- 为 warehouseName 添加 @TableField(exist = false) 注解标识非数据库字段
- 在 ShopStoreParam 参数类中添加 warehouseId 字段用于查询条件
- 更新实体类导入 TableField 注解支持
2026-02-06 20:02:51 +08:00
f8d134a330 feat(controller): 更新批量导入公司ID匹配逻辑
- 将公司名称匹配方法从精确匹配改为包含匹配模式
- 添加当事人角色字段的优先级处理机制:原告/上诉人 > 被告/被上诉人 > 其他当事人/第三人
- 在多个控制器中统一实现新的匹配策略
- 为CreditJudgmentDebtorController添加备用名称字段回退机制
- 移除原有的单一字段匹配参数,改用多角色字段组合匹配
2026-02-06 18:42:04 +08:00
6b401b8286 refactor(batch-import): 优化公司名称匹配算法
- 移除未使用的 HashSet 和 Set 导入
- 添加 patternLen 字段用于存储模式长度信息
- 修改 CompanyNameMatcher 构造函数以接收 patternLen 参数
- 在构建匹配器时收集并存储每个模式的长度
- 替换原有的 matchedIds 集合匹配逻辑
- 实现基于位置和长度的最优匹配选择算法
- 优先选择更长、更具体的匹配结果
- 处理相同位置不同长度的匹配冲突情况
- 改进模糊匹配的判断逻辑和性能表现
2026-02-06 18:27:13 +08:00
e7133f65c9 feat(case-filing): 更新批量导入功能以支持多角色公司名称匹配
- 实现了在文本中查找公司名称的功能,支持原告/上诉人、被告/被上诉人和其他当事人/第三人的多角色匹配
- 添加了特殊注释说明当事人列可能包含多个角色/名称的处理逻辑
- 调整了批量导入支持类的方法调用,改用新的公司ID刷新方法
- 修改了字段映射顺序,优先处理原告/上诉人字段,然后是被告/被上诉人和其他当事人/第三人字段
- 保持了原有的数据读取和设置逻辑不变
2026-02-06 18:10:18 +08:00
79612be1c6 feat(controller): 新增基于文本内容匹配企业名称的功能
- 在 BatchImportSupport 中新增 refreshCompanyIdByCompanyNameContainedInText 方法
- 实现 AC 自动机算法进行多模式字符串匹配
- 支持从文本字段中提取包含的企业名称并回填 companyId
- 添加 CompanyNameMatcher 内部类处理匹配逻辑
- 优化 CreditMediationController 使用新方法处理多方当事人字段
- 支持按租户分组避免跨租户误匹配
- 实现批量更新和事务处理机制
2026-02-06 17:46:53 +08:00
1b2d09049a refactor(credit): 调整判决债务人实体的金额字段命名
- 将CreditJudgmentDebtor实体中的amount字段描述从"执行标的(元)"改为"涉案金额"
- 将CreditJudgmentDebtorImportParam参数类中的amount字段重命名为involvedAmount
- 更新控制器中对金额字段的引用,使用新的involvedAmount字段名
- 移除参数类中重复的involvedAmount2字段定义
- 在导入模板示例中移除金额字段的默认值设置
2026-02-06 16:58:44 +08:00
a941e4a9ab feat(credit): 添加当事人信息字段到债务人实体
- 设置plaintiffAppellant字段值
- 设置appellee字段值
- 设置otherPartiesThirdParty字段值
- 支持上游XLS模板中的当事人姓名兼容性处理
2026-02-06 16:49:20 +08:00
bdc0acc097 Merge remote-tracking branch 'origin/master' 2026-02-06 16:42:07 +08:00
83cb7208a8 refactor(credit): 重构信用系统相关实体字段映射
- 将plaintiffUser和defendantUser字段替换为plaintiffAppellant和appellee
- 移除冗余的*2字段(如involvedAmount2、occurrenceTime2、courtName2)
- 统一使用标准字段进行数据映射避免重复逻辑
- 更新Excel注解标签以匹配新的字段命名规范
- 调整数据导入时的字段对应关系
2026-02-06 16:42:00 +08:00
d68a53e3d0 feat(entities): 添加实体字段并优化订单查询
- 在CreditJudgmentDebtor实体中新增原告/上诉人、被告/被上诉人和其他当事人字段
- 在CreditJudgmentDebtorImportParam参数类中添加对应的Excel映射字段
- 在GltTicketOrder实体中新增门店、配送员和仓库的名称地址相关字段
- 更新GltTicketOrderMapper.xml查询SQL,关联店铺和配送员信息
- 添加门店名称、地址、手机号及配送员姓名、手机号等非数据库字段映射
2026-02-06 16:41:47 +08:00
fe15c7120f feat(order): 实现订单地址快照功能
- 移除 GltTicketOrder 实体中 address 字段的 @TableField(exist = false) 注解
- 添加 ShopUserAddress 和 ShopUserAddressService 的依赖注入
- 在订单创建时实现地址快照逻辑,将用户地址信息保存到订单表
- 添加地址验证和权限检查功能
- 实现默认地址获取和地址拼接功能
- 添加订单参数空值校验
2026-02-06 13:43:08 +08:00
6285429753 feat(order): 添加订单实体扩展字段和用户关联查询
- 在GltTicketOrder实体中添加address、province、city、region等地址相关字段
- 在GltTicketOrder实体中添加nickname、phone、avatar等用户信息字段
- 所有新增字段均使用@TableField(exist = false)注解标记为非数据库字段
- 更新GltTicketOrderMapper.xml中的关联
2026-02-06 13:36:19 +08:00
46b5ce3971 1 2026-02-06 12:40:30 +08:00
4832929a11 1 2026-02-06 12:39:58 +08:00
1536f1780b feat(order): 添加订单快递单号同步到发货单功能
- 在订单更新时检查并同步快递单号到发货单表
- 使用LambdaQueryWrapper查询相关发货单记录
- 支持新增发货单或更新现有发货单的快递单号
- 添加异常处理避免同步失败影响主流程
- 实现手动录入和无需物流两种配送方式的处理
- 添加日志记录同步失败的情况便于排查问题
2026-02-06 02:27:56 +08:00
804a5a7bef feat(shop): 添加微信小程序发货信息自动同步功能
- 新增 ShopWechatShippingSyncService 接口及实现类
- 在订单发货时自动同步实物快递和无需物流的发货信息到微信后台
- 添加微信小程序 access_token 获取服务及缓存机制
- 优化订单发货逻辑,支持无需物流/自提订单的自动同步处理
- 添加详细的日志记录和异常处理机制
- 实现发货信息同步失败时的容错处理
2026-02-06 01:09:41 +08:00
60279fca4c fix(cache): 解决缓存中JSON null值导致的空指针问题
- 添加对历史缓存中JSON "null" 字符串的兼容处理
- 当缓存解析出null值时清理缓存并回源数据库
- 在CmsWebsiteServiceImpl中增加缓存清理逻辑
- 在ShopWebsiteServiceImpl中统一缓存异常处理机制
- 添加单元测试验证JSON null值场景的正确回退行为
2026-02-06 00:49:00 +08:00
2c076e2b0f feat(order): 添加送水订单配送时间和完整下单流程
- 在GltTicketOrder实体中新增sendTime字段用于记录配送时间
- 移除送水订单查询接口的权限验证要求,开放查询功能
- 实现完整的下单流程:验证登录用户、扣减水票、写入核销记录、创建订单
- 新增createWithWriteOff方法处理事务性下单操作,确保数据一致性
- 添加数据库行锁机制防止并发扣减问题
- 优化水票相关接口描述,明确为可用水票总数
- 移除水票日志添加接口的权限验证和操作日志注解
2026-02-06 00:15:31 +08:00
48cd2e1f7b feat(order): 添加送水订单配送时间和完整下单流程
- 在GltTicketOrder实体中新增sendTime字段用于记录配送时间
- 移除送水订单查询接口的权限验证要求,开放查询功能
- 实现完整的下单流程:验证登录用户、扣减水票、写入核销记录、创建订单
- 新增createWithWriteOff方法处理事务性下单操作,确保数据一致性
- 添加数据库行锁机制防止并发扣减问题
- 优化水票相关接口描述,明确为可用水票总数
- 移除水票日志添加接口的权限验证和操作日志注解
2026-02-06 00:15:20 +08:00
88afd149c3 feat(glt): 添加送水订单模块并优化经销商结算功能
- 新增送水订单实体类 GltTicketOrder 及其相关控制器、服务、映射器
- 添加送水订单参数类 GltTicketOrderParam 和 XML 映射配置
- 实现送水订单的增删改查、分页查询等完整 CRUD 功能
- 在经销商结算任务中引入分销设置功能,支持按级别控制分佣
- 更新总经销商分润计算逻辑,使用动态费率替代固定值
- 删除不再使用的中文字体修复脚本文件
- 重构经销商推荐佣金结算逻辑,支持最多三级分佣
- 优化订单状态检查逻辑,在退款流程中排除已完成订单
2026-02-05 18:51:53 +08:00
9672be2252 feat(settlement): 添加总经销商分润功能
- 引入 TOTAL_DEALER_DIVIDEND_RATE 常量用于总经销商分润计算
- 添加 findTotalDealerUserId 方法查找总经销商用户ID
- 新增 settleTotalDealerCommission 方法实现总经销商分润逻辑
- 修改 settleOneOrder 方法传入总经销商用户ID参数
- 更新 createDealerOrderRecord 方法支持总经销商分润记录
- 扩展 buildCommissionTraceComment 方法包含总经销商分润信息
- 添加 TotalDealerCommission 内部类封装总经销商分润数据
- 实现总经销商分润的幂等处理和日志记录功能
2026-02-05 15:47:40 +08:00
1107b9144f feat(settlement): 添加总经销商分润功能
- 引入 TOTAL_DEALER_DIVIDEND_RATE 常量用于总经销商分润计算
- 添加 findTotalDealerUserId 方法查找总经销商用户ID
- 新增 settleTotalDealerCommission 方法实现总经销商分润逻辑
- 修改 settleOneOrder 方法传入总经销商用户ID参数
- 更新 createDealerOrderRecord 方法支持总经销商分润记录
- 扩展 buildCommissionTraceComment 方法包含总经销商分润信息
- 添加 TotalDealerCommission 内部类封装总经销商分润数据
- 实现总经销商分润的幂等处理和日志记录功能
2026-02-05 15:43:02 +08:00
acc543b50a refactor(task): 将经销商订单结算任务从shop模块迁移到glt模块
- 修改包路径从com.gxwebsoft.shop.task到com.gxwebsoft.glt.task
- 调整模块间的依赖关系
- 更新相关的导入引用
2026-02-05 15:26:34 +08:00
b9c70bb4a3 perf(shop): 优化经销商设置列表排序逻辑
- 将默认排序字段从 create_time 改为 update_time
- 移除 PageParam 的排序方法,改用 Java Stream 的 sort 进行排序
- 添加空值检查,避免对空列表进行排序操作
- 使用 Comparator.nullsLast 处理空值情况
- 提升列表排序性能,减少不必要的对象创建
2026-02-05 15:15:51 +08:00
ee9ea88ce9 fix(entity): 修复ShopDealerSetting实体映射问题
- 添加TableField注解以正确映射数据库字段
- 将关键字字段名用反引号包围避免SQL语法冲突
- 更新XML映射文件中的字段引用为带反引号的形式
- 确保数据库查询与实体字段映射一致
2026-02-05 15:08:55 +08:00
093826435e feat(shop): 修改经销商设置实体ID生成策略并优化保存更新逻辑
- 将ShopDealerSetting实体的@TableId注解type从AUTO改为INPUT
- 新增saveOrUpdateByKey方法统一处理保存和更新操作
- 移除LambdaQueryWrapper手动构建的更新逻辑
- 简化控制器中的保存和更新接口实现
- 优化多租户场景下的数据操作逻辑
2026-02-05 15:06:50 +08:00
85a8d17194 feat(shop): 更新分销商设置表的保存和修改功能
- 添加 ShopDealerSettingSaveParam 参数类用于保存和修改操作
- 修改 save 方法使用新的参数类并实现实体构建逻辑
- 更新 update 方法使用 LambdaQueryWrapper 进行精确更新
- 添加 buildEntity 方法用于将参数转换为实体对象
- 实现 normalizeUpdateTime 方法处理时间戳溢出问题
- 添加租户ID默认值获取逻辑
- 增强更新操作的数据验证和错误处理机制
2026-02-05 14:33:17 +08:00
bbd41da1d3 fix(shop): 修复商城信息缓存解析逻辑
- 优化缓存数据解析流程,添加空值检查
- 当缓存解析失败时清理无效缓存键
- 改进异常处理机制,避免返回空数据
- 移除调试代码并完善日志记录
2026-02-05 10:46:28 +08:00
e4e10d46cc fix(mapper): 修复用户票券关联查询中的数据放大问题
- 修改 shop_order 表关联条件,从 order_no 改为 order_id + tenant_id 组合
- 添加 tenant_id 筛选避免跨租户数据污染
- 添加 deleted 字段过滤确保只关联未删除订单
- 将 pay_price 字段别名规范化为 camelCase 格式
2026-02-05 10:15:03 +08:00
195e90df5e fix(settlement): 修复经销商订单结算任务中的分润收入计算问题
- 注释掉三级经销商佣金计算逻辑以解决结算异常
- 保留直接推荐奖和二级分润收入的正常计算流程
- 防止因三级佣金计算导致的订单结算失败问题
2026-02-04 17:49:00 +08:00
c5da6f371b feat(order): 分离订单退款功能到独立接口并优化水票统计
- 将订单退款逻辑从update方法中分离到独立的refund接口
- 添加退款相关操作权限控制和参数验证
- 实现申请退款和同意退款两种状态的分别处理
- 新增水票总数统计功能,包括service、mapper和controller层实现
- 修改佣金注释文本从"第3级佣金"为"分润收入"
- 优化订单更新逻辑,禁止通过普通更新接口进行退款操作
2026-02-04 17:38:00 +08:00
51d3a029cc feat(shop): 添加订单支付功能支持
- 新增 OrderPrepayRequest DTO 用于处理支付请求参数
- 实现 prepay 接口支持 /pay、/prepay、/repay 多路径兼容
- 添加用户登录验证和租户权限校验机制
- 集成微信支付创建订单功能并返回支付信息
- 实现订单状态验证包括已支付、已删除、已过期等状态检查
- 支持通过订单ID或订单号查询并处理支付请求
- 添加支付类型参数处理和默认值设置逻辑
2026-02-04 15:57:34 +08:00
30c7e72a80 fix(order): 修复订单处理中的空指针异常和状态比较问题
- 添加 Objects 工具类导入用于安全的对象比较
- 修复 shopOrderNow 为空时的空指针异常
- 使用 Objects.equals 替换直接的 equals 比较避免 NPE
- 为发货状态变更逻辑添加清晰的注释说明
- 修复支付状态检查中的布尔值比较逻辑
2026-02-04 15:47:23 +08:00
36bf931274 feat(order): 更新订单发货状态为已发货
- 在订单状态更新时同步设置发货状态为"已发货"
- 确保订单处理流程中发货状态的一致性
2026-02-04 11:18:53 +08:00
58c755b715 feat(tickets): 实现套票购买量和赠送量的分离管理
- 购买量(buyQty)立即设置为可用状态
- 赠送量(giftQty)设置为冻结状态并按计划释放
- 修改总数量计算逻辑,将购买量和赠送量直接相加
- 更新套票记录中的可用数量和冻结数量字段
- 调整释放计划构建方法,仅基于赠送量进行释放规划
- 更新套票日志记录,区分可用量和冻结量的变化追踪
2026-02-04 10:24:46 +08:00
937e707890 refactor(glt): 优化套票发放服务逻辑
- 引入 IssueOutcome 枚举替代布尔返回值,提高代码可读性
- 恢复并完善订单时间条件查询逻辑
- 移除调试用的 System.out.println 语句
- 实现订单状态更新机制,发放完成后将订单置为已完成
- 增强幂等性处理,支持已处理订单的跳过逻辑
- 统一异常情况处理,各类失败场景返回对应枚举值
- 添加详细的注释说明业务逻辑和处理流程
2026-02-04 10:12:55 +08:00
9a79aff47d feat(ticket): 增加套票实体扩展字段和关联查询功能
- 在GltUserTicket实体中增加templateName、payPrice、goodsName、nickname、avatar、phone等扩展字段
- 在GltUserTicketLog实体中增加nickname、avatar、phone等用户信息扩展字段
- 在GltUserTicketRelease实体中增加nickname、avatar、phone等用户信息扩展字段
- 修改GltUserTicketMapper.xml实现用户表、模板表、订单表的LEFT JOIN关联查询
- 修改GltUserTicketLogMapper.xml实现用户表LEFT JOIN关联查询并优化搜索条件
- 修改GltUserTicketReleaseMapper.xml实现用户表LEFT JOIN关联查询并调整搜索逻辑
- 临时注释掉套票发放服务中的时间条件过滤逻辑
- 添加调试日志输出订单数量信息
2026-02-04 02:44:33 +08:00
d393de816f feat(ticket): 添加套票发放定时任务和核心服务
- 实现 GltTicketIssue10584Task 定时任务,每分钟扫描今日订单并生成套票账户
- 创建 GltTicketIssueService 服务,处理从订单生成用户套票和释放计划的完整流程
- 支持幂等处理,防止重复发放套票
- 实现月度释放计划生成功能,支持首期立即释放或次月释放模式
- 添加多租户支持和并发控制,确保任务执行安全
- 集成订单状态检查、套票模板验证和发放流水记录功能
2026-02-03 21:25:13 +08:00
27baa6ecf7 refactor(order): 移除套票发放相关代码
- 删除套票发放注释代码(冻结/可用、分期释放功能)
- 清理订单支付成功后的冗余业务逻辑
- 优化订单服务实现类的代码结构
2026-02-03 20:45:50 +08:00
d7a6b7cc94 feat(ticket): 实现套票分期释放功能核心数据结构
- 修改 GltUserTicketReleaseParam 中 id 和 userTicketId 类型从 Long 改为 Integer
- 移除 ShopOrderServiceImpl 中的 shopTicketBizService 依赖注入
- 注释掉订单支付成功后的套票发放调用
- 添加套票功能开发计划文档,定义套票模板、用户套票账户、释放计划和变更流水的核心概念
- 设计并创建套票相关数据库表,包括套票模板表、用户套票账户表、释放计划表和变更流水表
2026-02-03 20:37:11 +08:00
24133ef8a8 feat(glt): 添加水票功能模块
- 新增 GltTicketTemplate 实体类定义水票基础属性
- 实现 GltTicketTemplateController 提供水票管理API接口
- 创建 GltTicketTemplateMapper 和 XML 映射文件实现数据访问
- 定义 GltTicketTemplateParam 查询参数类
- 实现 GltTicketTemplateService 业务逻辑层接口
- 添加 GltUserTicket 实体类管理用户水票信息
- 实现 GltUserTicketController 控制器提供用户水票管理功能
- 新增 GltUserTicketLog 实体类记录消费日志
- 实现 GltUserTicketLogController 提供消费日志管理接口
- 完善相关 Mapper、Service 层接口及实现类
- 集成 Swagger 注解提供 API 文档支持
- 添加安全权限控制注解实现接口权限验证
2026-02-03 19:39:20 +08:00
35c155a1da feat(car): 实现车辆管理硬删除功能
- 在控制器中将删除方法改为硬删除(物理删除),绕过逻辑删除注解
- 在数据访问层添加硬删除SQL映射方法,支持单条和批量删除
- 在服务层定义硬删除接口方法并实现具体逻辑
- 添加空值校验确保删除操作的安全性
- 注释说明硬删除与逻辑删除的区别和用途
2026-02-02 18:33:54 +08:00
fc00728729 feat(order): 添加店铺ID字段到订单创建请求
- 在OrderCreateRequest DTO中新增storeId字段
- 为新字段添加Swagger文档注解支持
- 扩展订单创建功能以支持店铺维度的业务逻辑
2026-02-01 09:57:01 +08:00
279 changed files with 18507 additions and 4037 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()));
}
}

View File

@@ -0,0 +1,31 @@
# 套票(冻结/可用、分期释放)功能开发计划
## 目标
- 为“买N送M例如买1送4、起售例如20桶起售、按月释放例如每月释放10桶、可用未用完叠加”的套票/权益,提供后端可配置、可发放、可释放、可消费的能力。
## 核心概念
- 套票模板:按商品(goodsId)配置买赠规则、起售/起送、释放规则(每期释放数或释放期数)、首期释放时机。
- 用户套票账户:记录总量、可用量、冻结量、已用量、已释放量;绑定订单(用于幂等与追溯)。
- 释放计划:每期一条,到期后把冻结转可用(可用未用完自然叠加)。
- 变更流水:发放/释放/消费等都记录流水,便于对账与排查。
## 关键默认规则(可在模板里改)
- 仅赠送量进入套票账户默认不包含“购买量”本身如需“买20送80=总100”可在模板设置 `includeBuyQty=true`
- 首期释放:默认“支付成功当日/当刻”释放(`firstReleaseMode=0`);如需“下个月同日释放”,设 `firstReleaseMode=1`
- 每期释放:默认按 `monthlyReleaseQty`;如配置了 `releasePeriods`,则平均分摊并处理余数。
## 开发步骤(建议按顺序)
1. 需求确认与接入点确认(订单哪个节点发放、桶票如何消费/核销、退款是否回滚、释放日期规则)。
2. 数据表设计与SQL输出模板/账户/释放计划/流水)。
3. 实现套票模板后台CRUD接口。
4. 支付成功接入:在订单支付成功后发放套票账户+生成释放计划(幂等)。
5. 定时任务释放:扫描到期释放计划,执行“冻结->可用”转账(幂等)。
6. 消费扣减对用户可用量做扣减支持跨多套票账户FIFO扣减并落流水。
7. 测试与验收:至少覆盖(买赠计算、分期拆分、叠加逻辑、幂等、并发扣减)。
## 待确认点(不确认也可先按默认实现)
- 套票数量=赠送量?还是(购买量+赠送量)?
- “释放日期”是按支付时间的“日”还是固定每月某一天31号跨月如何处理
- 退款/取消:是否回滚未使用的可用/冻结?已释放但未用如何处理?
- 消费场景:在哪个业务点扣减(下单抵扣/提货核销/线下核销)?

View File

@@ -0,0 +1,121 @@
-- 套票(冻结/可用、分期释放)相关表
-- 说明:项目使用了 MyBatis-Plus 的 tenant 拦截;表内显式保留 tenant_id 字段并建议建立联合唯一索引。
-- 1) 套票模板(按商品配置)
CREATE TABLE IF NOT EXISTS shop_ticket_template (
id INT AUTO_INCREMENT PRIMARY KEY,
goods_id INT NOT NULL,
name VARCHAR(100) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
unit_name VARCHAR(20) NOT NULL DEFAULT '',
-- 起售/起送
min_buy_qty INT NOT NULL DEFAULT 1,
start_send_qty INT NOT NULL DEFAULT 1,
-- 买赠买1送4 => gift_multiplier=4
gift_multiplier INT NOT NULL DEFAULT 0,
-- 是否把购买量也计入套票总量(默认仅计入赠送量)
include_buy_qty TINYINT(1) NOT NULL DEFAULT 0,
-- 释放规则:二选一
-- A) 每期释放数量默认每月释放10
monthly_release_qty INT NOT NULL DEFAULT 10,
-- B) 总共释放多少期(若配置>0则按期数平均分摊
release_periods INT NULL,
-- 首期释放时机0=支付成功当刻1=下个月同日
first_release_mode INT NOT NULL DEFAULT 0,
comments VARCHAR(255) NULL,
sort_number INT NOT NULL DEFAULT 0,
user_id INT NULL,
deleted INT NOT NULL DEFAULT 0,
tenant_id INT NOT NULL,
create_time DATETIME NULL,
update_time DATETIME NULL,
UNIQUE KEY uk_ticket_template_tenant_goods (tenant_id, goods_id),
KEY idx_ticket_template_tenant (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2) 用户套票账户(一次购买通常生成一条)
CREATE TABLE IF NOT EXISTS shop_user_ticket (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
template_id INT NOT NULL,
goods_id INT NOT NULL,
user_id INT NOT NULL,
order_id INT NULL,
order_no VARCHAR(64) NULL,
order_goods_id INT NULL,
total_qty INT NOT NULL DEFAULT 0,
available_qty INT NOT NULL DEFAULT 0,
frozen_qty INT NOT NULL DEFAULT 0,
used_qty INT NOT NULL DEFAULT 0,
released_qty INT NOT NULL DEFAULT 0,
status INT NOT NULL DEFAULT 0,
deleted INT NOT NULL DEFAULT 0,
tenant_id INT NOT NULL,
create_time DATETIME NULL,
update_time DATETIME NULL,
KEY idx_user_ticket_user (tenant_id, user_id),
KEY idx_user_ticket_order (tenant_id, order_no),
UNIQUE KEY uk_user_ticket_order_goods (tenant_id, template_id, order_no, order_goods_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3) 释放计划/释放记录(每期一条,幂等执行)
CREATE TABLE IF NOT EXISTS shop_user_ticket_release (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_ticket_id BIGINT NOT NULL,
user_id INT NOT NULL,
period_no INT NOT NULL,
release_qty INT NOT NULL,
release_time DATETIME NOT NULL,
status INT NOT NULL DEFAULT 0, -- 0待释放 1已释放 2作废
released_time DATETIME NULL,
tenant_id INT NOT NULL,
create_time DATETIME NULL,
update_time DATETIME NULL,
UNIQUE KEY uk_ticket_release_period (tenant_id, user_ticket_id, period_no),
KEY idx_ticket_release_due (status, release_time),
KEY idx_ticket_release_user (tenant_id, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4) 套票变更流水(发放/释放/消费/回滚等)
CREATE TABLE IF NOT EXISTS shop_user_ticket_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_ticket_id BIGINT NOT NULL,
user_id INT NOT NULL,
change_type INT NOT NULL, -- 10发放 20释放 30消费 40回滚/退款
change_available INT NOT NULL DEFAULT 0,
change_frozen INT NOT NULL DEFAULT 0,
change_used INT NOT NULL DEFAULT 0,
available_after INT NOT NULL DEFAULT 0,
frozen_after INT NOT NULL DEFAULT 0,
used_after INT NOT NULL DEFAULT 0,
order_id INT NULL,
order_no VARCHAR(64) NULL,
remark VARCHAR(255) NULL,
tenant_id INT NOT NULL,
create_time DATETIME NULL,
update_time DATETIME NULL,
KEY idx_ticket_log_user (tenant_id, user_id),
KEY idx_ticket_log_ticket (tenant_id, user_ticket_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

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,14 @@
-- 水票配送订单:配送流程字段(需在数据库执行)
-- 表glt_ticket_order
ALTER TABLE glt_ticket_order2
ADD COLUMN delivery_status INT NULL DEFAULT 10 COMMENT '配送状态10待配送、20配送中、30待客户确认、40已完成',
ADD COLUMN send_start_time DATETIME NULL COMMENT '开始配送时间',
ADD COLUMN send_end_time DATETIME NULL COMMENT '确认送达时间',
ADD COLUMN send_end_img VARCHAR(512) NULL COMMENT '送达拍照留档图片URL',
ADD COLUMN receive_confirm_time DATETIME NULL COMMENT '客户确认收货时间',
ADD COLUMN receive_confirm_type INT NULL COMMENT '确认方式10手动、20照片、30超时';
CREATE INDEX idx_glt_ticket_order_rider_status ON glt_ticket_order (tenant_id, rider_id, delivery_status, deleted);
CREATE INDEX idx_glt_ticket_order_user_status ON glt_ticket_order (tenant_id, user_id, delivery_status, deleted);

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,98 @@
# 水票配送订单:后端提示词(可直接发给后端)
> 目标:把“水票下单后 -> 配送员接单/配送 -> 用户确认 -> 自动确认”的闭环放到后端,用明确的字段 + 状态机校验保证不越权、不乱跳状态、并发不重复接单。
>
> 接口前缀:当前后端控制器为 `@RequestMapping("/api/glt/glt-ticket-order")`,下文默认都带 `/api` 前缀(如需兼容旧路径,可做网关转发或保留旧路由)。
## 0) 角色与权限边界(务必在后端兜底)
- 用户端(小程序用户):只能看/操作自己的订单(`userId` = token userId
- 配送员端:只能看/操作分配给自己的订单(`riderId` = token userId / rider userId
- 管理端:按后台权限控制(可查询/派单/改状态,但仍需 tenantId 隔离)。
建议:对“配送员端接口”忽略前端传入的 `riderId/userId/tenantId`,统一从登录态注入,避免越权。
## 1) 订单查询(配送员端)
建议提供配送员专用分页接口:`GET /api/glt/glt-ticket-order/rider/page`(避免与后台管理分页混用)。
请支持以下筛选,并保证权限隔离:
- `deliveryStatus`10待配送、20配送中、30待客户确认、40已完成配送员端必要不传默认=10
- `keywords`:支持按地址/备注等模糊搜索(可选)
- 排序:建议默认 `sendTime asc, createTime desc`(或沿用后端默认排序,但请告知前端)
权限隔离要求(配送员端):
- 只返回当前登录配送员的订单:后端强制 `param.riderId = loginUserId`(前端传不传都一样)。
- `tenantId/deleted` 等同样后端兜底(只查当前租户、只查未删除)。
返回字段建议(配送员端用得上):
- 门店/仓库/用户/配送员的展示字段:`storeName/storeAddress/storePhone``warehouseName/warehouseAddress``nickname/phone/avatar``riderName/riderPhone`(现有 `pageRel/listRel` 已在做关联返回)。
- 导航相关(详见第 5 节):收货地址 `lat/lng`、门店/仓库 `lngAndLat`(可关联返回或做快照字段)。
## 2) 配送流程字段(建议后端落库并回传)
订单表建议确保有以下字段(当前前端已按这些字段做流程判断/展示):
- `riderId/riderName/riderPhone`:配送员信息
- `deliveryStatus`10/20/30/40
- `sendStartTime`:配送员点击“开始配送”的时间(建议 datetime
- `sendEndTime`:配送员点击“确认送达”的时间(建议 datetime
- `sendEndImg`:送达拍照留档图片 URL可选/必填由后端策略决定;建议 varchar(512)
- `receiveConfirmTime`:客户确认收货时间(建议 datetime
- `receiveConfirmType`10客户手动确认、20配送照片自动确认、30超时自动确认
数据库变更示例SQL见`docs/sql/2026-02-06_glt_ticket_order_delivery_fields.sql`)。
强烈建议把“配送状态”与“业务状态(status=0/1、deleted=0/1)”分开,避免混用:
- `status/deleted`:系统通用字段(现有逻辑)
- `deliveryStatus`:配送流程状态(本需求新增)
## 3) 状态流转与校验(强烈建议在后端做)
请在更新订单时做状态机校验,避免前端绕过流程:
- `10 -> 20`:仅允许订单属于当前配送员,且未开始/未送达
- `20 -> 30`:配送员确认送达(可带 `sendEndImg`
- `20/30 -> 40`:完成;来源可能是
- 客户手动确认(写 `receiveConfirmTime` + `receiveConfirmType=10`
- 配送照片直接完成(写 `receiveConfirmTime` + `receiveConfirmType=20`,并要求 `sendEndImg`
- 超时自动确认(写 `receiveConfirmTime` + `receiveConfirmType=30`,建议由定时任务执行)
并发/幂等建议(避免重复点击/重复请求带来的脏数据):
- 所有“状态变更接口”用条件更新实现原子校验:`UPDATE ... SET ... WHERE id=? AND rider_id=? AND delivery_status=? AND deleted=0`
- 对重复调用做幂等:
- `start`:如果已是 20 则直接返回成功;如果已到 30/40 返回“状态不允许”
- `delivered`:如果已是 30/40 则返回成功(或提示已送达);避免重复写 `sendEndTime`
- `confirm-receive`:如果已是 40 则返回成功(或提示已完成)
## 4) 建议新增/明确的接口能力
为了避免并发抢单/越权更新,建议新增更语义化的接口(或在 update 内做等价校验):
- 接单(抢单/派单):`POST /api/glt/glt-ticket-order/{id}/accept`
- 配送员端:后端原子校验:仅当 `rider_id IS NULL`(或为 0时才能写入当前 rider 信息
- 管理端派单:允许传 `riderId`,但需校验骑手归属门店/租户(如有该约束)
- 开始配送:`POST /api/glt/glt-ticket-order/{id}/start`
- 写:`sendStartTime=now``deliveryStatus=20`
- 校验:必须 `riderId=当前登录配送员` 且当前 `deliveryStatus=10`
- 确认送达:`POST /api/glt/glt-ticket-order/{id}/delivered`
- 入参:`sendEndImg`(可选/必填,按策略)
- 写:`sendEndTime=now``deliveryStatus=30``sendEndImg`
- 可选策略 A推荐可配置`sendEndImg` 必填且存在,则可直接 `deliveryStatus=40` 并写 `receiveConfirmTime/Type=20`
- 客户确认收货:`POST /api/glt/glt-ticket-order/{id}/confirm-receive`
- 校验:只能本人 `userId` 操作,且必须 `deliveryStatus=30`
- 写:`deliveryStatus=40``receiveConfirmTime=now``receiveConfirmType=10`
接口返回建议:
- 成功统一返回 `ApiResult.success(...)`
- 失败请返回明确 msg例如`无权限``订单不存在``订单状态不允许``订单已被其他配送员接单`
## 5) 为了“导航到收货地址/取货点”的字段补充(建议)
当前仅有 `address` 字符串,无法在小程序内 `openLocation` 精准导航;建议补充:
- 收货地址(推荐至少返回):`receiverName``receiverPhone``province/city/region/address/fullAddress``lat/lng`
- 取货点(门店/仓库,推荐至少返回):`store.lngAndLat``warehouse.lngAndLat`
实现方式二选一:
- 方式 A更快查询时关联 `shop_user_address``shop_store``shop_warehouse`,把经纬度字段透出给前端。
- 方式 B更稳下单时把收货地址的 `name/phone/lat/lng/fullAddress` 以及门店/仓库 `lngAndLat` 做快照写入订单,避免后续数据变更影响历史订单导航。
## 6)(可选但很有用)超时自动确认规则
- 建议后端提供可配置项:`autoConfirmHours`(例如 24h/48h
- 定时任务扫描:`deliveryStatus=30``sendEndTime < now - autoConfirmHours` 的订单
- 原子更新:只更新仍处于 30 的订单,写入 `deliveryStatus=40``receiveConfirmTime=now``receiveConfirmType=30`
## 7)(可选)字段/枚举建议(便于前后端对齐)
- `deliveryStatus`10待配送、20配送中、30待客户确认、40已完成
- `receiveConfirmType`10客户手动确认、20配送照片自动确认、30超时自动确认
- 时间字段统一返回格式:`yyyy-MM-dd HH:mm:ss`(与项目现有 `@JsonFormat` 风格一致)

View File

@@ -38,6 +38,11 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- spring-boot-web --> <!-- spring-boot-web -->
<dependency> <dependency>

View File

@@ -1,125 +0,0 @@
#!/bin/bash
###############################################################################
# 捐款证书中文乱码修复脚本
# 用途在运行中的Docker容器内安装中文字体
# 适用于:无法重新构建镜像的紧急情况
###############################################################################
set -e # 遇到错误立即退出
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 容器名称(可根据实际情况修改)
CONTAINER_NAME="websoft-api-container"
echo -e "${GREEN}================================${NC}"
echo -e "${GREEN}中文字体修复脚本${NC}"
echo -e "${GREEN}================================${NC}"
echo ""
# 检查容器是否存在
echo -e "${YELLOW}步骤 1/6: 检查容器状态...${NC}"
if ! docker ps | grep -q "$CONTAINER_NAME"; then
echo -e "${RED}错误:容器 $CONTAINER_NAME 未运行!${NC}"
echo "当前运行的容器:"
docker ps --format "table {{.Names}}\t{{.Status}}"
echo ""
read -p "请输入正确的容器名称: " CONTAINER_NAME
if [ -z "$CONTAINER_NAME" ]; then
echo -e "${RED}容器名称不能为空,退出。${NC}"
exit 1
fi
fi
echo -e "${GREEN}✓ 容器正在运行${NC}"
echo ""
# 检查是否已安装字体
echo -e "${YELLOW}步骤 2/6: 检查是否已安装中文字体...${NC}"
if docker exec "$CONTAINER_NAME" fc-list :lang=zh 2>/dev/null | grep -q "WenQuanYi"; then
echo -e "${GREEN}✓ 中文字体已安装${NC}"
docker exec "$CONTAINER_NAME" fc-list :lang=zh
echo ""
read -p "是否重新安装?(y/N): " REINSTALL
if [[ ! "$REINSTALL" =~ ^[Yy]$ ]]; then
echo "跳过安装,退出。"
exit 0
fi
else
echo -e "${YELLOW}未检测到中文字体,开始安装...${NC}"
fi
echo ""
# 安装字体工具
echo -e "${YELLOW}步骤 3/6: 安装字体工具...${NC}"
docker exec -u root "$CONTAINER_NAME" sh -c "apk add --no-cache fontconfig ttf-dejavu wget" || {
echo -e "${RED}✗ 字体工具安装失败${NC}"
exit 1
}
echo -e "${GREEN}✓ 字体工具安装成功${NC}"
echo ""
# 下载中文字体
echo -e "${YELLOW}步骤 4/6: 下载文泉驿微米黑字体...${NC}"
echo "正在从GitHub下载约10MB请稍候..."
docker exec -u root "$CONTAINER_NAME" sh -c "
wget -O /tmp/wqy-microhei.ttc https://github.com/anthonyfok/fonts-wqy-microhei/raw/master/wqy-microhei.ttc 2>&1 | grep -E 'Connecting|Length|saved' || true
" || {
echo -e "${YELLOW}GitHub下载失败尝试使用代理...${NC}"
docker exec -u root "$CONTAINER_NAME" sh -c "
wget -O /tmp/wqy-microhei.ttc https://ghproxy.com/https://github.com/anthonyfok/fonts-wqy-microhei/raw/master/wqy-microhei.ttc
" || {
echo -e "${RED}✗ 字体下载失败${NC}"
echo "请检查网络连接或手动下载字体文件。"
exit 1
}
}
echo -e "${GREEN}✓ 字体下载成功${NC}"
echo ""
# 安装字体
echo -e "${YELLOW}步骤 5/6: 安装字体文件...${NC}"
docker exec -u root "$CONTAINER_NAME" sh -c "
mkdir -p /usr/share/fonts/truetype/wqy && \
mv /tmp/wqy-microhei.ttc /usr/share/fonts/truetype/wqy/ && \
fc-cache -fv
" || {
echo -e "${RED}✗ 字体安装失败${NC}"
exit 1
}
echo -e "${GREEN}✓ 字体安装成功${NC}"
echo ""
# 验证安装
echo -e "${YELLOW}步骤 6/6: 验证字体安装...${NC}"
FONT_COUNT=$(docker exec "$CONTAINER_NAME" fc-list :lang=zh | wc -l)
if [ "$FONT_COUNT" -gt 0 ]; then
echo -e "${GREEN}✓ 中文字体验证成功!${NC}"
echo "已安装的中文字体:"
docker exec "$CONTAINER_NAME" fc-list :lang=zh
else
echo -e "${RED}✗ 字体验证失败${NC}"
exit 1
fi
echo ""
# 完成提示
echo -e "${GREEN}================================${NC}"
echo -e "${GREEN}修复完成!${NC}"
echo -e "${GREEN}================================${NC}"
echo ""
echo "后续步骤:"
echo "1. 不需要重启容器,字体已生效"
echo "2. 重新生成捐款证书即可看到效果"
echo "3. 如果仍有问题,请查看容器日志:"
echo " docker logs -f $CONTAINER_NAME"
echo ""
echo -e "${YELLOW}注意:${NC}"
echo "- 此修复方法在容器重启后会失效"
echo "- 建议后续使用更新后的Dockerfile重新构建镜像"
echo "- 详细文档请参考docs/chinese-font-fix-guide.md"
echo ""

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

@@ -6,8 +6,9 @@
<sql id="selectSql"> <sql id="selectSql">
SELECT a.*, b.title as parentName, b.position as parentPosition, c.name as modelName SELECT a.*, b.title as parentName, b.position as parentPosition, c.name as modelName
FROM cms_navigation a FROM cms_navigation a
LEFT JOIN cms_navigation b ON a.parent_id = b.navigation_id LEFT JOIN cms_navigation b ON a.parent_id = b.navigation_id AND b.deleted = 0 AND b.tenant_id = a.tenant_id
LEFT JOIN cms_model c ON a.model = c.model <!-- c.model = 0 会触发 MySQL 字符串转数字比较,导致几乎所有模型都匹配,从而把导航行成倍放大 -->
LEFT JOIN cms_model c ON a.model = c.model AND c.deleted = 0 AND c.tenant_id = a.tenant_id
<where> <where>
<if test="param.navigationId != null"> <if test="param.navigationId != null">
AND a.navigation_id = #{param.navigationId} AND a.navigation_id = #{param.navigationId}

View File

@@ -337,9 +337,16 @@ public class CmsWebsiteServiceImpl extends ServiceImpl<CmsWebsiteMapper, CmsWebs
if (StrUtil.isNotBlank(siteInfo)) { if (StrUtil.isNotBlank(siteInfo)) {
log.info("从缓存获取网站信息租户ID: {}", tenantId); log.info("从缓存获取网站信息租户ID: {}", tenantId);
try { try {
return JSONUtil.parseObject(siteInfo, ShopVo.class); // 兼容历史缓存JSON "null" 会被解析为 null此时应视为未命中并回源数据库。
ShopVo cacheVo = JSONUtil.parseObject(siteInfo, ShopVo.class);
if (cacheVo != null) {
return cacheVo;
}
log.warn("网站信息缓存命中但内容为空(null)清理缓存后回源数据库租户ID: {}", tenantId);
redisUtil.delete(cacheKey);
} catch (Exception e) { } catch (Exception e) {
log.warn("缓存解析失败,从数据库重新获取: {}", e.getMessage()); log.warn("缓存解析失败,清理缓存后从数据库重新获取: {}", e.getMessage());
redisUtil.delete(cacheKey);
} }
} }

View File

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

View File

@@ -67,6 +67,7 @@ public class SecurityConfig {
"/api/shop/wx-login/**", "/api/shop/wx-login/**",
"/api/shop/wx-native-pay/**", "/api/shop/wx-native-pay/**",
"/api/shop/wx-pay/**", "/api/shop/wx-pay/**",
"/api/system/wx-pay/**",
"/api/bszx/bszx-pay/notify/**", "/api/bszx/bszx-pay/notify/**",
"/api/wxWorkQrConnect", "/api/wxWorkQrConnect",
"/WW_verify_QMv7HoblYU6z63bb.txt", "/WW_verify_QMv7HoblYU6z63bb.txt",

View File

@@ -50,15 +50,26 @@ public class EnvironmentAwarePaymentService {
return null; 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); Payment envPayment = clonePayment(payment);
String notifyUrl = getEnvironmentNotifyUrl(); String notifyUrl = getEnvironmentNotifyUrl();
log.info("环境感知支付配置(兜底) - 环境: {}, 原始回调为空,兜底回调: {}", activeProfile, notifyUrl);
log.info("环境感知支付配置 - 环境: {}, 原始回调: {}, 调整后回调: {}",
activeProfile, payment.getNotifyUrl(), notifyUrl);
envPayment.setNotifyUrl(notifyUrl); envPayment.setNotifyUrl(notifyUrl);
return envPayment; return envPayment;
} }

View File

@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package cn.afterturn.easypoi.excel.entity; package com.gxwebsoft.common.core.web;
import cn.afterturn.easypoi.excel.entity.ExcelBaseParams;
import cn.afterturn.easypoi.handler.inter.IExcelVerifyHandler; import cn.afterturn.easypoi.handler.inter.IExcelVerifyHandler;
import lombok.Data; import lombok.Data;

View File

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

View File

@@ -102,7 +102,7 @@
AND a.deleted = 0 AND a.deleted = 0
</if> </if>
<if test="param.roleId != null"> <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>
<if test="param.userIds != null"> <if test="param.userIds != null">
AND a.user_id IN 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 LEFT JOIN gxwebsoft_core.sys_user_referee h ON a.user_id = h.user_id and h.deleted = 0
WHERE a.user_id = #{userId} WHERE a.user_id = #{userId}
AND a.deleted = 0 AND a.deleted = 0
LIMIT 1
</select> </select>
</mapper> </mapper>

View File

@@ -0,0 +1,18 @@
package com.gxwebsoft.common.system.service;
/**
* 微信小程序 access_token 获取服务(按租户)。
*
* <p>用于调用微信小程序开放接口(例如:上传发货信息)。</p>
*/
public interface WxMiniappAccessTokenService {
/**
* 获取指定租户的小程序 access_token内部带缓存
*
* @param tenantId 租户ID
* @return access_token
*/
String getAccessToken(Integer tenantId);
}

View File

@@ -0,0 +1,108 @@
package com.gxwebsoft.common.system.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.system.service.WxMiniappAccessTokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import static com.gxwebsoft.common.core.constants.RedisConstants.ACCESS_TOKEN_KEY;
import static com.gxwebsoft.common.core.constants.RedisConstants.MP_WX_KEY;
/**
* 微信小程序 access_token 获取实现(按租户)。
*
* <p>复用现有缓存结构:
* <ul>
* <li>小程序配置Redis key = {@code mp-weixin:{tenantId}}value 为 JSON包含 appId/appSecret</li>
* <li>access_tokenRedis key = {@code access-token:{tenantId}}value 为微信返回的 JSON 字符串</li>
* </ul>
* </p>
*/
@Slf4j
@Service
public class WxMiniappAccessTokenServiceImpl implements WxMiniappAccessTokenService {
@Resource
private RedisUtil redisUtil;
@Override
public String getAccessToken(Integer tenantId) {
if (tenantId == null) {
throw new BusinessException("tenantId 不能为空");
}
final String tokenCacheKey = ACCESS_TOKEN_KEY + ":" + tenantId;
// 1) 优先从缓存取(兼容 JSON 或纯字符串 token 的历史格式)
String cachedValue = redisUtil.get(tokenCacheKey);
if (StrUtil.isNotBlank(cachedValue)) {
try {
JSONObject cachedJson = JSON.parseObject(cachedValue);
String accessToken = cachedJson.getString("access_token");
if (StrUtil.isNotBlank(accessToken)) {
return accessToken;
}
} catch (Exception ignore) {
// 旧格式:直接存 token
return cachedValue;
}
}
// 2) 缓存没有则从租户配置获取 appId/appSecret
final String wxConfigKey = MP_WX_KEY + tenantId;
final String wxConfigValue = redisUtil.get(wxConfigKey);
if (StrUtil.isBlank(wxConfigValue)) {
throw new BusinessException("未找到微信小程序配置请检查缓存key: " + wxConfigKey);
}
JSONObject wxConfig;
try {
wxConfig = JSON.parseObject(wxConfigValue);
} catch (Exception e) {
throw new BusinessException("微信小程序配置格式错误: " + e.getMessage());
}
final String appId = wxConfig.getString("appId");
final String appSecret = wxConfig.getString("appSecret");
if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) {
throw new BusinessException("微信小程序配置不完整(appId/appSecret)");
}
// 3) 调用微信接口获取 token
final String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
+ "&appid=" + appId + "&secret=" + appSecret;
String result = HttpUtil.get(apiUrl);
JSONObject json = JSON.parseObject(result);
if (json.containsKey("errcode") && json.getIntValue("errcode") != 0) {
Integer errcode = json.getInteger("errcode");
String errmsg = json.getString("errmsg");
throw new BusinessException("获取小程序access_token失败: " + errmsg + " (errcode: " + errcode + ")");
}
String accessToken = json.getString("access_token");
Integer expiresIn = json.getInteger("expires_in");
if (StrUtil.isBlank(accessToken)) {
throw new BusinessException("获取小程序access_token失败: access_token为空");
}
// 4) 缓存微信原始 JSON与现有实现保持一致提前5分钟过期
long ttlSeconds = 7000L;
if (expiresIn != null && expiresIn > 300) {
ttlSeconds = expiresIn - 300L;
}
redisUtil.set(tokenCacheKey, result, ttlSeconds, TimeUnit.SECONDS);
log.info("获取小程序access_token成功 - tenantId={}, ttlSeconds={}", tenantId, ttlSeconds);
return accessToken;
}
}

View File

@@ -1,22 +1,31 @@
package com.gxwebsoft.credit.controller; package com.gxwebsoft.credit.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.toolkit.SqlRunner;
import com.gxwebsoft.credit.entity.CreditCompany; import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.service.CreditCompanyService; import com.gxwebsoft.credit.service.CreditCompanyService;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -45,6 +54,66 @@ public class BatchImportSupport {
return requiresNewTx.execute(status -> supplier.get()); 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 static final class CompanyIdRefreshStats {
public final boolean anyDataRead; public final boolean anyDataRead;
public final int updated; public final int updated;
@@ -109,6 +178,43 @@ public class BatchImportSupport {
nameGetter); 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。 * 按多列“当事人/企业名称”匹配 CreditCompany(name / matchName) 并回填 companyId。
* *
@@ -131,6 +237,44 @@ public class BatchImportSupport {
SFunction<T, Integer> tenantIdGetter, SFunction<T, Integer> tenantIdGetter,
Supplier<T> patchFactory, Supplier<T> patchFactory,
SFunction<T, String>... nameGetters) { 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); boolean onlyNullFlag = (onlyNull == null) || Boolean.TRUE.equals(onlyNull);
if (nameGetters == null || nameGetters.length == 0) { if (nameGetters == null || nameGetters.length == 0) {
@@ -211,6 +355,8 @@ public class BatchImportSupport {
// 3.1) 查询当前租户下的 companyId 映射 // 3.1) 查询当前租户下的 companyId 映射
LinkedHashMap<String, Integer> companyIdByName = new LinkedHashMap<>(); LinkedHashMap<String, Integer> companyIdByName = new LinkedHashMap<>();
LinkedHashMap<String, Integer> ambiguousByName = 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<>(); LinkedHashSet<String> nameSet = new LinkedHashSet<>();
for (T row : tenantRows) { for (T row : tenantRows) {
if (row == null) { if (row == null) {
@@ -240,6 +386,13 @@ public class BatchImportSupport {
if (c == null || c.getId() == null) { if (c == null || c.getId() == null) {
continue; 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.getName()), c.getId());
addCompanyNameMapping(companyIdByName, ambiguousByName, normalizeCompanyName(c.getMatchName()), c.getId()); addCompanyNameMapping(companyIdByName, ambiguousByName, normalizeCompanyName(c.getMatchName()), c.getId());
} }
@@ -312,6 +465,12 @@ public class BatchImportSupport {
T patch = patchFactory.get(); T patch = patchFactory.get();
idSetter.accept(patch, id); idSetter.accept(patch, id);
companyIdSetter.accept(patch, companyId); 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); hasDataSetter.accept(patch, Boolean.TRUE);
updates.add(patch); updates.add(patch);
if (updates.size() >= batchSize) { if (updates.size() >= batchSize) {
@@ -336,6 +495,225 @@ public class BatchImportSupport {
return new CompanyIdRefreshStats(true, updated, matched, notFound, ambiguous); return new CompanyIdRefreshStats(true, updated, matched, notFound, ambiguous);
} }
/**
* 按“文本字段包含企业名称”的方式匹配 CreditCompany(name / matchName) 并回填 companyId。
*
* <p>适用场景:某些表的“当事人/第三人”字段会包含多个角色+姓名/企业,例如:
* 申请执行人 - 张三 被执行人 - 某某有限公司。此时无法按整格等值匹配。</p>
*
* <p>实现:按租户加载企业 name/matchName 构建多模式匹配AC 自动机),在文本中查找出现的企业名。</p>
*
* <p>列优先级:按 textGetters 的顺序尝试;若某列匹配到唯一企业则采用,否则继续下一列。</p>
*/
@SafeVarargs
public final <T> CompanyIdRefreshStats refreshCompanyIdByCompanyNameContainedInText(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,
SFunction<T, Boolean> hasDataGetter,
BiConsumer<T, Boolean> hasDataSetter,
SFunction<T, Integer> tenantIdGetter,
Supplier<T> patchFactory,
SFunction<T, String>... textGetters) {
return refreshCompanyIdByCompanyNameContainedInText(
service,
creditCompanyService,
currentTenantId,
onlyNull,
limit,
idGetter,
idSetter,
companyIdGetter,
companyIdSetter,
hasDataGetter,
hasDataSetter,
tenantIdGetter,
patchFactory,
null,
textGetters
);
}
/**
* refreshCompanyIdByCompanyNameContainedInText 的增强版:支持对 CreditCompany 查询追加条件(例如仅匹配一级企业)。
*/
@SafeVarargs
public final <T> CompanyIdRefreshStats refreshCompanyIdByCompanyNameContainedInText(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,
SFunction<T, Boolean> hasDataGetter,
BiConsumer<T, Boolean> hasDataSetter,
SFunction<T, Integer> tenantIdGetter,
Supplier<T> patchFactory,
Consumer<LambdaQueryChainWrapper<CreditCompany>> companyQueryCustomizer,
SFunction<T, String>... textGetters) {
boolean onlyNullFlag = (onlyNull == null) || Boolean.TRUE.equals(onlyNull);
if (textGetters == null || textGetters.length == 0) {
return new CompanyIdRefreshStats(false, 0, 0, 0, 0);
}
// 1) 读取待处理数据(仅取必要字段)
@SuppressWarnings({"rawtypes", "unchecked"})
SFunction<T, ?>[] selectColumns = (SFunction<T, ?>[]) new SFunction[4 + textGetters.length];
int colIdx = 0;
selectColumns[colIdx++] = idGetter;
selectColumns[colIdx++] = companyIdGetter;
selectColumns[colIdx++] = hasDataGetter;
selectColumns[colIdx++] = tenantIdGetter;
for (SFunction<T, String> tg : textGetters) {
selectColumns[colIdx++] = tg;
}
var query = service.lambdaQuery()
.select(selectColumns)
.eq(currentTenantId != null, tenantIdGetter, currentTenantId)
.and(w -> {
for (int i = 0; i < textGetters.length; i++) {
if (i == 0) {
w.isNotNull(textGetters[i]);
} else {
w.or().isNotNull(textGetters[i]);
}
}
});
if (onlyNullFlag) {
query.and(w -> w.isNull(companyIdGetter).or().eq(companyIdGetter, 0));
}
if (limit != null && limit > 0) {
query.last("limit " + Math.min(limit, 200000));
}
List<T> rows = query.list();
if (CollectionUtils.isEmpty(rows)) {
return new CompanyIdRefreshStats(false, 0, 0, 0, 0);
}
// 2) 按租户分组(避免跨租户误匹配)
Map<Integer, List<T>> rowsByTenant = new LinkedHashMap<>();
int missingTenant = 0;
for (T row : rows) {
if (row == null) {
continue;
}
Integer tenantId = currentTenantId != null ? currentTenantId : tenantIdGetter.apply(row);
if (tenantId == null) {
missingTenant++;
continue;
}
rowsByTenant.computeIfAbsent(tenantId, k -> new ArrayList<>()).add(row);
}
int updated = 0;
int matched = 0;
int notFound = 0;
int ambiguous = 0;
final int batchSize = 500;
List<T> updates = new ArrayList<>(batchSize);
for (Map.Entry<Integer, List<T>> entry : rowsByTenant.entrySet()) {
Integer tenantId = entry.getKey();
List<T> tenantRows = entry.getValue();
if (tenantId == null || CollectionUtils.isEmpty(tenantRows)) {
continue;
}
// 2.1) 构建当前租户的企业名匹配器
LambdaQueryChainWrapper<CreditCompany> companyQuery = creditCompanyService.lambdaQuery()
.select(CreditCompany::getId, CreditCompany::getName, CreditCompany::getMatchName, CreditCompany::getTenantId)
.eq(CreditCompany::getTenantId, tenantId)
;
if (companyQueryCustomizer != null) {
companyQueryCustomizer.accept(companyQuery);
}
List<CreditCompany> companies = companyQuery.list();
CompanyNameMatcher matcher = CompanyNameMatcher.build(companies);
// 2.2) 匹配并回填
for (T row : tenantRows) {
if (row == null) {
continue;
}
Integer resolvedCompanyId = null;
boolean hasAmbiguous = false;
for (SFunction<T, String> tg : textGetters) {
String text = tg.apply(row);
CompanyNameMatcher.MatchResult r = matcher.match(text);
if (r.ambiguous) {
hasAmbiguous = true;
continue;
}
if (r.companyId != null) {
resolvedCompanyId = r.companyId;
break;
}
}
if (resolvedCompanyId == null) {
if (hasAmbiguous) {
ambiguous++;
} else {
notFound++;
}
continue;
}
matched++;
Integer oldCompanyId = companyIdGetter.apply(row);
Boolean oldHasData = hasDataGetter.apply(row);
boolean needUpdate;
if (onlyNullFlag) {
needUpdate = (oldCompanyId == null) || oldCompanyId == 0;
} else {
needUpdate = oldCompanyId == null || !resolvedCompanyId.equals(oldCompanyId);
}
if (!Boolean.TRUE.equals(oldHasData)) {
needUpdate = true;
}
if (!needUpdate) {
continue;
}
Integer id = idGetter.apply(row);
if (id == null) {
continue;
}
T patch = patchFactory.get();
idSetter.accept(patch, id);
companyIdSetter.accept(patch, resolvedCompanyId);
hasDataSetter.accept(patch, Boolean.TRUE);
updates.add(patch);
if (updates.size() >= batchSize) {
List<T> batch = new ArrayList<>(updates);
updates.clear();
updated += runInNewTx(() -> service.updateBatchById(batch, batchSize) ? batch.size() : 0);
}
}
}
if (currentTenantId == null && missingTenant > 0) {
notFound += missingTenant;
}
if (!updates.isEmpty()) {
List<T> batch = new ArrayList<>(updates);
updates.clear();
updated += runInNewTx(() -> service.updateBatchById(batch, batchSize) ? batch.size() : 0);
}
return new CompanyIdRefreshStats(true, updated, matched, notFound, ambiguous);
}
/** /**
* 批量 upsert优先按 code 匹配code 为空时按 name 匹配。 * 批量 upsert优先按 code 匹配code 为空时按 name 匹配。
*/ */
@@ -751,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 入库条数)。 * 批量失败时降级逐行:允许调用方自定义“成功条数”的计算口径(例如:仅统计 insert 入库条数)。
* *
@@ -852,4 +1319,211 @@ public class BatchImportSupport {
// SFunction 是 getter method ref直接调用即可 // SFunction 是 getter method ref直接调用即可
return idColumn.apply(entity); return idColumn.apply(entity);
} }
/**
* Multi-pattern substring matcher for company names (CreditCompany.name / matchName).
* Uses an AhoCorasick automaton to scan each text only once.
*/
private static final class CompanyNameMatcher {
private static final int MIN_PATTERN_LEN = 4; // Avoid false positives in free text (e.g. person names)
private static final class Node {
final Map<Character, Integer> next = new HashMap<>();
final List<Integer> out = new ArrayList<>();
int fail = 0;
}
private final List<Node> nodes;
private final int[] patternCompanyId; // 0 means ambiguous
private final int[] patternLen;
private CompanyNameMatcher(List<Node> nodes, int[] patternCompanyId, int[] patternLen) {
this.nodes = nodes;
this.patternCompanyId = patternCompanyId;
this.patternLen = patternLen;
}
static CompanyNameMatcher build(List<CreditCompany> companies) {
List<Node> nodes = new ArrayList<>();
nodes.add(new Node()); // root
Map<String, Integer> patternIndex = new HashMap<>();
List<Integer> companyIds = new ArrayList<>();
List<Integer> patternLens = new ArrayList<>();
if (!CollectionUtils.isEmpty(companies)) {
for (CreditCompany c : companies) {
if (c == null || c.getId() == null) {
continue;
}
addPattern(nodes, patternIndex, companyIds, patternLens, normalizeCompanyName(c.getName()), c.getId());
addPattern(nodes, patternIndex, companyIds, patternLens, normalizeCompanyName(c.getMatchName()), c.getId());
}
}
int[] patternCompanyId = new int[companyIds.size()];
for (int i = 0; i < companyIds.size(); i++) {
patternCompanyId[i] = companyIds.get(i) != null ? companyIds.get(i) : 0;
}
int[] patternLen = new int[patternLens.size()];
for (int i = 0; i < patternLens.size(); i++) {
patternLen[i] = patternLens.get(i) != null ? patternLens.get(i) : 0;
}
buildFailureLinks(nodes);
return new CompanyNameMatcher(nodes, patternCompanyId, patternLen);
}
private static void addPattern(List<Node> nodes,
Map<String, Integer> patternIndex,
List<Integer> companyIds,
List<Integer> patternLens,
String pattern,
Integer companyId) {
if (pattern == null || companyId == null) {
return;
}
if (pattern.length() < MIN_PATTERN_LEN) {
return;
}
Integer existingIndex = patternIndex.get(pattern);
if (existingIndex != null) {
// Same pattern maps to multiple companies -> mark ambiguous.
Integer oldCompanyId = companyIds.get(existingIndex);
if (oldCompanyId != null && !oldCompanyId.equals(companyId)) {
companyIds.set(existingIndex, null);
}
return;
}
int state = 0;
for (int i = 0; i < pattern.length(); i++) {
char ch = pattern.charAt(i);
Integer next = nodes.get(state).next.get(ch);
if (next == null) {
next = nodes.size();
nodes.get(state).next.put(ch, next);
nodes.add(new Node());
}
state = next;
}
int idx = companyIds.size();
companyIds.add(companyId);
patternLens.add(pattern.length());
nodes.get(state).out.add(idx);
patternIndex.put(pattern, idx);
}
private static void buildFailureLinks(List<Node> nodes) {
Queue<Integer> q = new ArrayDeque<>();
// Init depth-1 nodes
for (Map.Entry<Character, Integer> e : nodes.get(0).next.entrySet()) {
int s = e.getValue();
nodes.get(s).fail = 0;
q.add(s);
}
while (!q.isEmpty()) {
int r = q.poll();
for (Map.Entry<Character, Integer> e : nodes.get(r).next.entrySet()) {
char a = e.getKey();
int s = e.getValue();
q.add(s);
int state = nodes.get(r).fail;
while (state != 0 && !nodes.get(state).next.containsKey(a)) {
state = nodes.get(state).fail;
}
Integer fs = nodes.get(state).next.get(a);
nodes.get(s).fail = (fs != null) ? fs : 0;
// Merge outputs from fail state
List<Integer> out = nodes.get(nodes.get(s).fail).out;
if (!out.isEmpty()) {
nodes.get(s).out.addAll(out);
}
}
}
}
static final class MatchResult {
final Integer companyId; // unique match
final boolean ambiguous;
MatchResult(Integer companyId, boolean ambiguous) {
this.companyId = companyId;
this.ambiguous = ambiguous;
}
}
MatchResult match(String text) {
String v = normalizeCompanyName(text);
if (v == null) {
return new MatchResult(null, false);
}
int state = 0;
Integer bestCompanyId = null;
int bestStart = Integer.MAX_VALUE;
int bestLen = -1;
boolean ambiguous = false;
for (int i = 0; i < v.length(); i++) {
char ch = v.charAt(i);
while (state != 0 && !nodes.get(state).next.containsKey(ch)) {
state = nodes.get(state).fail;
}
Integer next = nodes.get(state).next.get(ch);
state = next != null ? next : 0;
List<Integer> out = nodes.get(state).out;
if (out.isEmpty()) {
continue;
}
for (Integer idx : out) {
if (idx == null || idx < 0 || idx >= patternCompanyId.length) {
continue;
}
int cid = patternCompanyId[idx];
// Pattern exists but maps to multiple companies -> ignore this hit, keep looking for a unique one.
if (cid == 0) {
continue;
}
int len = (idx < patternLen.length) ? patternLen[idx] : 0;
int start = len > 0 ? (i - len + 1) : i;
if (bestCompanyId == null) {
bestCompanyId = cid;
bestStart = start;
bestLen = len;
continue;
}
if (start < bestStart) {
bestCompanyId = cid;
bestStart = start;
bestLen = len;
continue;
}
if (start == bestStart) {
// Prefer the longer (more specific) match at the same position.
if (len > bestLen) {
bestCompanyId = cid;
bestLen = len;
continue;
}
// Same position + same length but different companyId -> truly ambiguous.
if (len == bestLen && !bestCompanyId.equals(cid)) {
ambiguous = true;
break;
}
}
}
if (ambiguous) {
break;
}
}
if (ambiguous) {
return new MatchResult(null, true);
}
return new MatchResult(bestCompanyId, false);
}
}
} }

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

View File

@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditBankruptcyController extends BaseController {
@Operation(summary = "删除破产重整") @Operation(summary = "删除破产重整")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditBankruptcyService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditBankruptcy.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditBankruptcyController extends BaseController {
@Operation(summary = "批量删除破产重整") @Operation(summary = "批量删除破产重整")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditBankruptcyService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditBankruptcy.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -196,8 +195,16 @@ public class CreditBankruptcyController extends BaseController {
Set<Integer> touchedCompanyIds = new HashSet<>(); Set<Integer> touchedCompanyIds = new HashSet<>();
try { try {
ExcelImportSupport.ImportResult<CreditBankruptcyImportParam> importResult = ExcelImportSupport.readAnySheet( // Prefer importing from the explicit tab name "破产重整" when present.
file, CreditBankruptcyImportParam.class, this::isEmptyImportRow); // 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(); List<CreditBankruptcyImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows(); int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows(); int usedHeadRows = importResult.getHeadRows();
@@ -263,38 +270,13 @@ public class CreditBankruptcyController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditBankruptcyService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditBankruptcyService, CreditBankruptcy::getCode,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -308,38 +290,13 @@ public class CreditBankruptcyController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditBankruptcyService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditBankruptcyService, CreditBankruptcy::getCode,
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;
},
errorMessages errorMessages
); );
} }
@@ -359,7 +316,7 @@ public class CreditBankruptcyController extends BaseController {
/** /**
* 批量导入历史破产重整(仅解析“历史破产重整”选项卡) * 批量导入历史破产重整(仅解析“历史破产重整”选项卡)
* 规则:案号/唯一标识相同则覆盖更新recommend++ 记录更新次数);不存在则插入。 * 规则:使用数据库唯一索引约束,重复数据不导入。
*/ */
@PreAuthorize("hasAuthority('credit:creditBankruptcy:save')") @PreAuthorize("hasAuthority('credit:creditBankruptcy:save')")
@Operation(summary = "批量导入历史破产重整") @Operation(summary = "批量导入历史破产重整")
@@ -392,8 +349,10 @@ public class CreditBankruptcyController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCode = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号"); Map<String, String> urlByCode = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
LinkedHashMap<String, CreditBankruptcy> latestByCode = new LinkedHashMap<>(); final int chunkSize = 500;
LinkedHashMap<String, Integer> latestRowByCode = new LinkedHashMap<>(); final int mpBatchSize = 500;
List<CreditBankruptcy> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
CreditBankruptcyImportParam param = list.get(i); CreditBankruptcyImportParam param = list.get(i);
@@ -429,121 +388,42 @@ public class CreditBankruptcyController extends BaseController {
item.setDeleted(0); item.setDeleted(0);
} }
latestByCode.put(item.getCode(), item); if (item.getRecommend() == null) {
latestRowByCode.put(item.getCode(), excelRowNumber); 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) { } catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage()); errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace(); 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()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditBankruptcyService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate( mpBatchSize,
creditBankruptcyService, CreditBankruptcy::getCode,
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 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.BatchParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditBranch; import com.gxwebsoft.credit.entity.CreditBranch;
import com.gxwebsoft.credit.param.CreditBranchImportParam; import com.gxwebsoft.credit.param.CreditBranchImportParam;
import com.gxwebsoft.credit.param.CreditBranchParam; import com.gxwebsoft.credit.param.CreditBranchParam;
@@ -104,7 +105,7 @@ public class CreditBranchController extends BaseController {
@Operation(summary = "删除分支机构") @Operation(summary = "删除分支机构")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditBranchService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditBranch.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditBranchController extends BaseController {
@Operation(summary = "批量删除分支机构") @Operation(summary = "批量删除分支机构")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditBranchService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditBranch.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditBranchController extends BaseController {
CreditBranch::getName, CreditBranch::getName,
CreditBranch::getCompanyId, CreditBranch::getCompanyId,
CreditBranch::setCompanyId, CreditBranch::setCompanyId,
CreditBranch::setCompanyName,
CreditBranch::getHasData, CreditBranch::getHasData,
CreditBranch::setHasData, CreditBranch::setHasData,
CreditBranch::getTenantId, CreditBranch::getTenantId,
@@ -210,6 +212,11 @@ public class CreditBranchController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null; Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = 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 chunkSize = 500;
final int mpBatchSize = 500; final int mpBatchSize = 500;
@@ -229,6 +236,9 @@ public class CreditBranchController extends BaseController {
if (item.getCompanyId() == null && companyId != null) { if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId); item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
} }
if (item.getUserId() == null && currentUserId != null) { if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId); item.setUserId(currentUserId);
@@ -259,38 +269,13 @@ public class CreditBranchController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditBranchService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditBranchService, CreditBranch::getName,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -304,38 +289,13 @@ public class CreditBranchController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditBranchService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditBranchService, CreditBranch::getName,
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;
},
errorMessages errorMessages
); );
} }

View File

@@ -23,10 +23,8 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -101,7 +99,7 @@ public class CreditBreachOfTrustController extends BaseController {
@Operation(summary = "删除失信被执行人") @Operation(summary = "删除失信被执行人")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditBreachOfTrustService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditBreachOfTrust.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -134,7 +132,7 @@ public class CreditBreachOfTrustController extends BaseController {
@Operation(summary = "批量删除失信被执行人") @Operation(summary = "批量删除失信被执行人")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditBreachOfTrustService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditBreachOfTrust.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -156,7 +154,9 @@ public class CreditBreachOfTrustController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( // Party columns may contain multiple roles/names; match if any company name is contained in the text.
// Priority: 原告/上诉人 > 被告/被上诉人 > 其他当事人/第三人
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNameContainedInText(
creditBreachOfTrustService, creditBreachOfTrustService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -164,13 +164,15 @@ public class CreditBreachOfTrustController extends BaseController {
limit, limit,
CreditBreachOfTrust::getId, CreditBreachOfTrust::getId,
CreditBreachOfTrust::setId, CreditBreachOfTrust::setId,
CreditBreachOfTrust::getPlaintiffAppellant,
CreditBreachOfTrust::getCompanyId, CreditBreachOfTrust::getCompanyId,
CreditBreachOfTrust::setCompanyId, CreditBreachOfTrust::setCompanyId,
CreditBreachOfTrust::getHasData, CreditBreachOfTrust::getHasData,
CreditBreachOfTrust::setHasData, CreditBreachOfTrust::setHasData,
CreditBreachOfTrust::getTenantId, CreditBreachOfTrust::getTenantId,
CreditBreachOfTrust::new CreditBreachOfTrust::new,
CreditBreachOfTrust::getPlaintiffAppellant,
CreditBreachOfTrust::getAppellee,
CreditBreachOfTrust::getOtherPartiesThirdParty
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {
@@ -260,38 +262,13 @@ public class CreditBreachOfTrustController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditBreachOfTrustService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditBreachOfTrustService, CreditBreachOfTrust::getCaseNumber,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -305,38 +282,13 @@ public class CreditBreachOfTrustController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditBreachOfTrustService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditBreachOfTrustService, CreditBreachOfTrust::getCaseNumber,
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;
},
errorMessages errorMessages
); );
} }
@@ -356,7 +308,7 @@ public class CreditBreachOfTrustController extends BaseController {
/** /**
* 批量导入历史失信被执行人(仅解析“历史失信被执行人”选项卡) * 批量导入历史失信被执行人(仅解析“历史失信被执行人”选项卡)
* 规则:案号相同则覆盖更新recommend++ 记录更新次数);案号不存在则插入。 * 规则:使用数据库唯一索引约束,重复数据不导入。
*/ */
@PreAuthorize("hasAuthority('credit:creditBreachOfTrust:save')") @PreAuthorize("hasAuthority('credit:creditBreachOfTrust:save')")
@Operation(summary = "批量导入历史失信被执行人") @Operation(summary = "批量导入历史失信被执行人")
@@ -390,8 +342,10 @@ public class CreditBreachOfTrustController extends BaseController {
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号"); Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
// 同案号多条:以导入文件中“最后一条”为准(视为最新) // 同案号多条:以导入文件中“最后一条”为准(视为最新)
LinkedHashMap<String, CreditBreachOfTrust> latestByCaseNumber = new LinkedHashMap<>(); final int chunkSize = 500;
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>(); final int mpBatchSize = 500;
List<CreditBreachOfTrust> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
CreditBreachOfTrustImportParam param = list.get(i); CreditBreachOfTrustImportParam param = list.get(i);
@@ -429,121 +383,42 @@ public class CreditBreachOfTrustController extends BaseController {
// 历史导入的数据统一标记为“失效” // 历史导入的数据统一标记为“失效”
item.setDataStatus("失效"); item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item); if (item.getRecommend() == null) {
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber); 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) { } catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage()); errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace(); 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()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditBreachOfTrustService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate( mpBatchSize,
creditBreachOfTrustService, CreditBreachOfTrust::getCaseNumber,
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 errorMessages
); );
} }

View File

@@ -104,7 +104,7 @@ public class CreditCaseFilingController extends BaseController {
@Operation(summary = "删除司法大数据") @Operation(summary = "删除司法大数据")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCaseFilingService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditCaseFiling.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +137,7 @@ public class CreditCaseFilingController extends BaseController {
@Operation(summary = "批量删除司法大数据") @Operation(summary = "批量删除司法大数据")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCaseFilingService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditCaseFiling.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -159,7 +159,9 @@ public class CreditCaseFilingController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( // Special: party columns may contain multiple roles/names; match if any company name is contained in the text.
// Priority: 原告/上诉人 > 被告/被上诉人 > 其他当事人/第三人
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNameContainedInText(
creditCaseFilingService, creditCaseFilingService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -167,13 +169,15 @@ public class CreditCaseFilingController extends BaseController {
limit, limit,
CreditCaseFiling::getId, CreditCaseFiling::getId,
CreditCaseFiling::setId, CreditCaseFiling::setId,
CreditCaseFiling::getAppellee,
CreditCaseFiling::getCompanyId, CreditCaseFiling::getCompanyId,
CreditCaseFiling::setCompanyId, CreditCaseFiling::setCompanyId,
CreditCaseFiling::getHasData, CreditCaseFiling::getHasData,
CreditCaseFiling::setHasData, CreditCaseFiling::setHasData,
CreditCaseFiling::getTenantId, CreditCaseFiling::getTenantId,
CreditCaseFiling::new CreditCaseFiling::new,
CreditCaseFiling::getPlaintiffAppellant,
CreditCaseFiling::getAppellee,
CreditCaseFiling::getOtherPartiesThirdParty
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {
@@ -261,38 +265,13 @@ public class CreditCaseFilingController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditCaseFilingService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditCaseFilingService, CreditCaseFiling::getCaseNumber,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -306,38 +285,13 @@ public class CreditCaseFilingController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditCaseFilingService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditCaseFilingService, CreditCaseFiling::getCaseNumber,
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;
},
errorMessages errorMessages
); );
} }
@@ -355,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 = "删除企业") @Operation(summary = "删除企业")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCompanyService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditCompany.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -132,7 +132,7 @@ public class CreditCompanyController extends BaseController {
@Operation(summary = "批量删除企业") @Operation(summary = "批量删除企业")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCompanyService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditCompany.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -149,6 +149,7 @@ public class CreditCompanyController extends BaseController {
List<String> errorMessages = new ArrayList<>(); List<String> errorMessages = new ArrayList<>();
int insertedCount = 0; int insertedCount = 0;
Set<String> touchedMatchNames = new HashSet<>(); Set<String> touchedMatchNames = new HashSet<>();
String refreshWarning = null;
try { try {
List<CreditCompanyImportParam> list = null; 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()) { if (errorMessages.isEmpty()) {
return success("成功入库" + insertedCount + "条数据", null); String msg = "成功入库" + insertedCount + "条数据";
if (refreshWarning != null) {
msg = msg + "" + refreshWarning;
}
return success(msg, null);
} else { } 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) { } 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.BatchParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditCompetitor; import com.gxwebsoft.credit.entity.CreditCompetitor;
import com.gxwebsoft.credit.param.CreditCompetitorImportParam; import com.gxwebsoft.credit.param.CreditCompetitorImportParam;
import com.gxwebsoft.credit.param.CreditCompetitorParam; import com.gxwebsoft.credit.param.CreditCompetitorParam;
@@ -104,7 +105,7 @@ public class CreditCompetitorController extends BaseController {
@Operation(summary = "删除竞争对手") @Operation(summary = "删除竞争对手")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCompetitorService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditCompetitor.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditCompetitorController extends BaseController {
@Operation(summary = "批量删除竞争对手") @Operation(summary = "批量删除竞争对手")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCompetitorService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditCompetitor.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditCompetitorController extends BaseController {
CreditCompetitor::getName, CreditCompetitor::getName,
CreditCompetitor::getCompanyId, CreditCompetitor::getCompanyId,
CreditCompetitor::setCompanyId, CreditCompetitor::setCompanyId,
CreditCompetitor::setCompanyName,
CreditCompetitor::getHasData, CreditCompetitor::getHasData,
CreditCompetitor::setHasData, CreditCompetitor::setHasData,
CreditCompetitor::getTenantId, CreditCompetitor::getTenantId,
@@ -212,6 +214,11 @@ public class CreditCompetitorController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = ExcelImportSupport.readUrlByKey( Map<String, String> urlByName = ExcelImportSupport.readUrlByKey(
file, usedSheetIndex, usedTitleRows, usedHeadRows, "企业名称"); 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 chunkSize = 500;
final int mpBatchSize = 500; final int mpBatchSize = 500;
@@ -222,7 +229,7 @@ public class CreditCompetitorController extends BaseController {
CreditCompetitorImportParam param = list.get(i); CreditCompetitorImportParam param = list.get(i);
try { try {
CreditCompetitor item = convertImportParamToEntity(param); CreditCompetitor item = convertImportParamToEntity(param);
// name 才是持久化字段companyName 为关联查询的临时字段exist=false导入时不应使用 // name 为竞争对手企业名称companyName 为主体企业名称
if (!ImportHelper.isBlank(item.getName())) { if (!ImportHelper.isBlank(item.getName())) {
String link = urlByName.get(item.getName().trim()); String link = urlByName.get(item.getName().trim());
if (!ImportHelper.isBlank(link)) { if (!ImportHelper.isBlank(link)) {
@@ -232,6 +239,9 @@ public class CreditCompetitorController extends BaseController {
if (item.getCompanyId() == null && companyId != null) { if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId); item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
} }
if (item.getUserId() == null && currentUserId != null) { if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId); item.setUserId(currentUserId);
@@ -262,38 +272,13 @@ public class CreditCompetitorController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditCompetitorService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditCompetitorService, CreditCompetitor::getName,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -307,38 +292,13 @@ public class CreditCompetitorController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditCompetitorService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditCompetitorService, CreditCompetitor::getName,
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;
},
errorMessages errorMessages
); );
} }

View File

@@ -104,7 +104,7 @@ public class CreditCourtAnnouncementController extends BaseController {
@Operation(summary = "删除法院公告司法大数据") @Operation(summary = "删除法院公告司法大数据")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCourtAnnouncementService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditCourtAnnouncement.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +137,7 @@ public class CreditCourtAnnouncementController extends BaseController {
@Operation(summary = "批量删除法院公告司法大数据") @Operation(summary = "批量删除法院公告司法大数据")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCourtAnnouncementService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditCourtAnnouncement.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -159,7 +159,9 @@ public class CreditCourtAnnouncementController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( // Party columns may contain multiple roles/names; match if any company name is contained in the text.
// Priority: 原告/上诉人 > 被告/被上诉人 > 其他当事人/第三人
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNameContainedInText(
creditCourtAnnouncementService, creditCourtAnnouncementService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -167,13 +169,15 @@ public class CreditCourtAnnouncementController extends BaseController {
limit, limit,
CreditCourtAnnouncement::getId, CreditCourtAnnouncement::getId,
CreditCourtAnnouncement::setId, CreditCourtAnnouncement::setId,
CreditCourtAnnouncement::getAppellee,
CreditCourtAnnouncement::getCompanyId, CreditCourtAnnouncement::getCompanyId,
CreditCourtAnnouncement::setCompanyId, CreditCourtAnnouncement::setCompanyId,
CreditCourtAnnouncement::getHasData, CreditCourtAnnouncement::getHasData,
CreditCourtAnnouncement::setHasData, CreditCourtAnnouncement::setHasData,
CreditCourtAnnouncement::getTenantId, CreditCourtAnnouncement::getTenantId,
CreditCourtAnnouncement::new CreditCourtAnnouncement::new,
CreditCourtAnnouncement::getPlaintiffAppellant,
CreditCourtAnnouncement::getAppellee,
CreditCourtAnnouncement::getOtherPartiesThirdParty
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {
@@ -261,38 +265,13 @@ public class CreditCourtAnnouncementController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtAnnouncementService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditCourtAnnouncementService, CreditCourtAnnouncement::getCaseNumber,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -306,38 +285,13 @@ public class CreditCourtAnnouncementController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtAnnouncementService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditCourtAnnouncementService, CreditCourtAnnouncement::getCaseNumber,
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;
},
errorMessages errorMessages
); );
} }
@@ -355,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.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditCourtSessionController extends BaseController {
@Operation(summary = "删除开庭公告司法大数据") @Operation(summary = "删除开庭公告司法大数据")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCourtSessionService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditCourtSession.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditCourtSessionController extends BaseController {
@Operation(summary = "批量删除开庭公告司法大数据") @Operation(summary = "批量删除开庭公告司法大数据")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCourtSessionService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditCourtSession.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -160,7 +159,9 @@ public class CreditCourtSessionController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( // Party columns may contain multiple roles/names; match if any company name is contained in the text.
// Priority: 原告/上诉人 > 被告/被上诉人 > 其他当事人/第三人
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNameContainedInText(
creditCourtSessionService, creditCourtSessionService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -168,13 +169,15 @@ public class CreditCourtSessionController extends BaseController {
limit, limit,
CreditCourtSession::getId, CreditCourtSession::getId,
CreditCourtSession::setId, CreditCourtSession::setId,
CreditCourtSession::getAppellee,
CreditCourtSession::getCompanyId, CreditCourtSession::getCompanyId,
CreditCourtSession::setCompanyId, CreditCourtSession::setCompanyId,
CreditCourtSession::getHasData, CreditCourtSession::getHasData,
CreditCourtSession::setHasData, CreditCourtSession::setHasData,
CreditCourtSession::getTenantId, CreditCourtSession::getTenantId,
CreditCourtSession::new CreditCourtSession::new,
CreditCourtSession::getPlaintiffAppellant,
CreditCourtSession::getAppellee,
CreditCourtSession::getOtherPartiesThirdParty
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {
@@ -265,38 +268,13 @@ public class CreditCourtSessionController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtSessionService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditCourtSessionService, CreditCourtSession::getCaseNumber,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -310,38 +288,13 @@ public class CreditCourtSessionController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtSessionService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditCourtSessionService, CreditCourtSession::getCaseNumber,
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;
},
errorMessages errorMessages
); );
} }
@@ -361,7 +314,7 @@ public class CreditCourtSessionController extends BaseController {
/** /**
* 批量导入历史开庭公告(仅解析“历史开庭公告”选项卡) * 批量导入历史开庭公告(仅解析“历史开庭公告”选项卡)
* 规则:案号相同则覆盖更新recommend++ 记录更新次数);案号不存在则插入。 * 规则:使用数据库唯一索引约束,重复数据不导入。
*/ */
@PreAuthorize("hasAuthority('credit:creditCourtSession:save')") @PreAuthorize("hasAuthority('credit:creditCourtSession:save')")
@Operation(summary = "批量导入历史开庭公告司法大数据") @Operation(summary = "批量导入历史开庭公告司法大数据")
@@ -394,8 +347,10 @@ public class CreditCourtSessionController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号"); Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
LinkedHashMap<String, CreditCourtSession> latestByCaseNumber = new LinkedHashMap<>(); final int chunkSize = 500;
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>(); final int mpBatchSize = 500;
List<CreditCourtSession> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
CreditCourtSessionImportParam param = list.get(i); CreditCourtSessionImportParam param = list.get(i);
@@ -433,121 +388,42 @@ public class CreditCourtSessionController extends BaseController {
// 历史导入的数据统一标记为“失效” // 历史导入的数据统一标记为“失效”
item.setDataStatus("失效"); item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item); if (item.getRecommend() == null) {
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber); 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) { } catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage()); errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace(); 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()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditCourtSessionService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate( mpBatchSize,
creditCourtSessionService, CreditCourtSession::getCaseNumber,
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 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.BatchParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditCustomer; import com.gxwebsoft.credit.entity.CreditCustomer;
import com.gxwebsoft.credit.param.CreditCustomerImportParam; import com.gxwebsoft.credit.param.CreditCustomerImportParam;
import com.gxwebsoft.credit.param.CreditCustomerParam; import com.gxwebsoft.credit.param.CreditCustomerParam;
@@ -24,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -100,7 +102,7 @@ public class CreditCustomerController extends BaseController {
@Operation(summary = "删除客户") @Operation(summary = "删除客户")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditCustomerService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditCustomer.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -133,7 +135,7 @@ public class CreditCustomerController extends BaseController {
@Operation(summary = "批量删除客户") @Operation(summary = "批量删除客户")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditCustomerService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditCustomer.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -166,6 +168,7 @@ public class CreditCustomerController extends BaseController {
CreditCustomer::getName, CreditCustomer::getName,
CreditCustomer::getCompanyId, CreditCustomer::getCompanyId,
CreditCustomer::setCompanyId, CreditCustomer::setCompanyId,
CreditCustomer::setCompanyName,
CreditCustomer::getHasData, CreditCustomer::getHasData,
CreditCustomer::setHasData, CreditCustomer::setHasData,
CreditCustomer::getTenantId, CreditCustomer::getTenantId,
@@ -207,6 +210,11 @@ public class CreditCustomerController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null; Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "客户"); 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 chunkSize = 500;
final int mpBatchSize = 500; final int mpBatchSize = 500;
@@ -219,7 +227,8 @@ public class CreditCustomerController extends BaseController {
try { try {
CreditCustomer item = convertImportParamToEntity(param); CreditCustomer item = convertImportParamToEntity(param);
if (!ImportHelper.isBlank(item.getName())) { 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)) { if (!ImportHelper.isBlank(link)) {
item.setUrl(link.trim()); item.setUrl(link.trim());
} }
@@ -227,6 +236,9 @@ public class CreditCustomerController extends BaseController {
if (item.getCompanyId() == null && companyId != null) { if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId); item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
} }
if (item.getUserId() == null && currentUserId != null) { if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId); item.setUserId(currentUserId);
@@ -256,116 +268,7 @@ public class CreditCustomerController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += persistImportChunk(chunkItems, chunkRowNumbers, mpBatchSize, errorMessages);
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
);
chunkItems.clear(); chunkItems.clear();
chunkRowNumbers.clear(); chunkRowNumbers.clear();
} }
@@ -376,114 +279,7 @@ public class CreditCustomerController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += persistImportChunk(chunkItems, chunkRowNumbers, mpBatchSize, errorMessages);
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
);
} }
creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.CUSTOMER, touchedCompanyIds); creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.CUSTOMER, touchedCompanyIds);
@@ -554,19 +350,71 @@ public class CreditCustomerController extends BaseController {
return value.trim(); 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(); Throwable mostSpecificCause = e.getMostSpecificCause();
String message = mostSpecificCause != null ? mostSpecificCause.getMessage() : e.getMessage(); String message = mostSpecificCause != null ? mostSpecificCause.getMessage() : e.getMessage();
if (message == null) { if (message == null) {
return false; return false;
} }
String lower = message.toLowerCase(); String lower = message.toLowerCase();
if (!lower.contains("duplicate")) { return lower.contains("duplicate") && lower.contains("key");
return false;
}
return lower.contains("credit_customer.name")
|| lower.contains("for key 'name'")
|| lower.contains("for key `name`");
} }
} }

View File

@@ -104,7 +104,7 @@ public class CreditDeliveryNoticeController extends BaseController {
@Operation(summary = "删除送达公告司法大数据") @Operation(summary = "删除送达公告司法大数据")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditDeliveryNoticeService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditDeliveryNotice.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +137,7 @@ public class CreditDeliveryNoticeController extends BaseController {
@Operation(summary = "批量删除送达公告司法大数据") @Operation(summary = "批量删除送达公告司法大数据")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditDeliveryNoticeService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditDeliveryNotice.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -159,7 +159,9 @@ public class CreditDeliveryNoticeController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( // Party columns may contain multiple roles/names; match if any company name is contained in the text.
// Priority: 原告/上诉人 > 被告/被上诉人 > 其他当事人/第三人
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNameContainedInText(
creditDeliveryNoticeService, creditDeliveryNoticeService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -167,13 +169,15 @@ public class CreditDeliveryNoticeController extends BaseController {
limit, limit,
CreditDeliveryNotice::getId, CreditDeliveryNotice::getId,
CreditDeliveryNotice::setId, CreditDeliveryNotice::setId,
CreditDeliveryNotice::getOtherPartiesThirdParty,
CreditDeliveryNotice::getCompanyId, CreditDeliveryNotice::getCompanyId,
CreditDeliveryNotice::setCompanyId, CreditDeliveryNotice::setCompanyId,
CreditDeliveryNotice::getHasData, CreditDeliveryNotice::getHasData,
CreditDeliveryNotice::setHasData, CreditDeliveryNotice::setHasData,
CreditDeliveryNotice::getTenantId, CreditDeliveryNotice::getTenantId,
CreditDeliveryNotice::new CreditDeliveryNotice::new,
CreditDeliveryNotice::getPlaintiffAppellant,
CreditDeliveryNotice::getAppellee,
CreditDeliveryNotice::getOtherPartiesThirdParty
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {
@@ -262,38 +266,13 @@ public class CreditDeliveryNoticeController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditDeliveryNoticeService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditDeliveryNoticeService, CreditDeliveryNotice::getCaseNumber,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -307,38 +286,13 @@ public class CreditDeliveryNoticeController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditDeliveryNoticeService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditDeliveryNoticeService, CreditDeliveryNotice::getCaseNumber,
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;
},
errorMessages errorMessages
); );
} }
@@ -356,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.BatchParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditExternal; import com.gxwebsoft.credit.entity.CreditExternal;
import com.gxwebsoft.credit.param.CreditExternalImportParam; import com.gxwebsoft.credit.param.CreditExternalImportParam;
import com.gxwebsoft.credit.param.CreditExternalParam; import com.gxwebsoft.credit.param.CreditExternalParam;
@@ -104,7 +105,7 @@ public class CreditExternalController extends BaseController {
@Operation(summary = "删除对外投资") @Operation(summary = "删除对外投资")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditExternalService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditExternal.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditExternalController extends BaseController {
@Operation(summary = "批量删除对外投资") @Operation(summary = "批量删除对外投资")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditExternalService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditExternal.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditExternalController extends BaseController {
CreditExternal::getName, CreditExternal::getName,
CreditExternal::getCompanyId, CreditExternal::getCompanyId,
CreditExternal::setCompanyId, CreditExternal::setCompanyId,
CreditExternal::setCompanyName,
CreditExternal::getHasData, CreditExternal::getHasData,
CreditExternal::setHasData, CreditExternal::setHasData,
CreditExternal::getTenantId, CreditExternal::getTenantId,
@@ -211,6 +213,11 @@ public class CreditExternalController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null; Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "被投资企业名称"); 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 chunkSize = 500;
final int mpBatchSize = 500; final int mpBatchSize = 500;
@@ -230,6 +237,9 @@ public class CreditExternalController extends BaseController {
if (item.getCompanyId() == null && companyId != null) { if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId); item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
} }
if (item.getUserId() == null && currentUserId != null) { if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId); item.setUserId(currentUserId);
@@ -260,38 +270,13 @@ public class CreditExternalController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditExternalService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditExternalService, CreditExternal::getName,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -305,38 +290,13 @@ public class CreditExternalController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditExternalService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditExternalService, CreditExternal::getName,
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;
},
errorMessages errorMessages
); );
} }

View File

@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditFinalVersionController extends BaseController {
@Operation(summary = "删除终本案件") @Operation(summary = "删除终本案件")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditFinalVersionService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditFinalVersion.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditFinalVersionController extends BaseController {
@Operation(summary = "批量删除终本案件") @Operation(summary = "批量删除终本案件")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditFinalVersionService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditFinalVersion.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -160,7 +159,9 @@ public class CreditFinalVersionController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( // Party columns may contain multiple roles/names; match if any company name is contained in the text.
// Priority: 原告/上诉人 > 被告/被上诉人 > 其他当事人/第三人
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNameContainedInText(
creditFinalVersionService, creditFinalVersionService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -168,13 +169,15 @@ public class CreditFinalVersionController extends BaseController {
limit, limit,
CreditFinalVersion::getId, CreditFinalVersion::getId,
CreditFinalVersion::setId, CreditFinalVersion::setId,
CreditFinalVersion::getAppellee,
CreditFinalVersion::getCompanyId, CreditFinalVersion::getCompanyId,
CreditFinalVersion::setCompanyId, CreditFinalVersion::setCompanyId,
CreditFinalVersion::getHasData, CreditFinalVersion::getHasData,
CreditFinalVersion::setHasData, CreditFinalVersion::setHasData,
CreditFinalVersion::getTenantId, CreditFinalVersion::getTenantId,
CreditFinalVersion::new CreditFinalVersion::new,
CreditFinalVersion::getPlaintiffAppellant,
CreditFinalVersion::getAppellee,
CreditFinalVersion::getOtherPartiesThirdParty
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {
@@ -265,38 +268,13 @@ public class CreditFinalVersionController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditFinalVersionService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditFinalVersionService, CreditFinalVersion::getCaseNumber,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -310,38 +288,13 @@ public class CreditFinalVersionController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditFinalVersionService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditFinalVersionService, CreditFinalVersion::getCaseNumber,
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;
},
errorMessages errorMessages
); );
} }
@@ -361,7 +314,7 @@ public class CreditFinalVersionController extends BaseController {
/** /**
* 批量导入历史终本案件(仅解析“历史终本案件”选项卡) * 批量导入历史终本案件(仅解析“历史终本案件”选项卡)
* 规则:案号相同则覆盖更新recommend++ 记录更新次数);案号不存在则插入。 * 规则:使用数据库唯一索引约束,重复数据不导入。
*/ */
@PreAuthorize("hasAuthority('credit:creditFinalVersion:save')") @PreAuthorize("hasAuthority('credit:creditFinalVersion:save')")
@Operation(summary = "批量导入历史终本案件") @Operation(summary = "批量导入历史终本案件")
@@ -394,8 +347,10 @@ public class CreditFinalVersionController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号"); Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
LinkedHashMap<String, CreditFinalVersion> latestByCaseNumber = new LinkedHashMap<>(); final int chunkSize = 500;
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>(); final int mpBatchSize = 500;
List<CreditFinalVersion> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
CreditFinalVersionImportParam param = list.get(i); CreditFinalVersionImportParam param = list.get(i);
@@ -433,121 +388,42 @@ public class CreditFinalVersionController extends BaseController {
// 历史导入的数据统一标记为“失效” // 历史导入的数据统一标记为“失效”
item.setDataStatus("失效"); item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item); if (item.getRecommend() == null) {
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber); 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) { } catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage()); errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace(); 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()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditFinalVersionService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate( mpBatchSize,
creditFinalVersionService, CreditFinalVersion::getCaseNumber,
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 errorMessages
); );
} }

View File

@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditGqdjController extends BaseController {
@Operation(summary = "删除股权冻结") @Operation(summary = "删除股权冻结")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditGqdjService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditGqdj.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditGqdjController extends BaseController {
@Operation(summary = "批量删除股权冻结") @Operation(summary = "批量删除股权冻结")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditGqdjService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditGqdj.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -356,38 +355,13 @@ public class CreditGqdjController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditGqdjService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditGqdjService, CreditGqdj::getCaseNumber,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -401,38 +375,13 @@ public class CreditGqdjController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditGqdjService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditGqdjService, CreditGqdj::getCaseNumber,
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;
},
errorMessages errorMessages
); );
} }
@@ -452,7 +401,7 @@ public class CreditGqdjController extends BaseController {
/** /**
* 批量导入历史股权冻结(仅解析“历史股权冻结”选项卡) * 批量导入历史股权冻结(仅解析“历史股权冻结”选项卡)
* 规则:执行通知文书号/案号相同则覆盖更新recommend++ 记录更新次数);不存在则插入。 * 规则:使用数据库唯一索引约束,重复数据不导入。
*/ */
@PreAuthorize("hasAuthority('credit:creditGqdj:save')") @PreAuthorize("hasAuthority('credit:creditGqdj:save')")
@Operation(summary = "批量导入历史股权冻结司法大数据") @Operation(summary = "批量导入历史股权冻结司法大数据")
@@ -547,8 +496,10 @@ public class CreditGqdjController extends BaseController {
} }
} }
LinkedHashMap<String, CreditGqdj> latestByCaseNumber = new LinkedHashMap<>(); final int chunkSize = 500;
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>(); final int mpBatchSize = 500;
List<CreditGqdj> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
CreditGqdjImportParam param = list.get(i); CreditGqdjImportParam param = list.get(i);
@@ -588,123 +539,44 @@ public class CreditGqdjController extends BaseController {
item.setDeleted(0); item.setDeleted(0);
} }
// 历史导入的数据统一标记为“失效” // 历史导入的数据统一标记为“失效”
item.setDataStatus("失效"); item.setDataType("失效");
latestByCaseNumber.put(item.getCaseNumber(), item); if (item.getRecommend() == null) {
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber); 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) { } catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage()); errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace(); 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()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditGqdjService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate( mpBatchSize,
creditGqdjService, CreditGqdj::getCaseNumber,
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 errorMessages
); );
} }
@@ -795,7 +667,7 @@ public class CreditGqdjController extends BaseController {
} else { } else {
entity.setDataStatus(param.getDataStatus()); entity.setDataStatus(param.getDataStatus());
} }
entity.setDataType("股权冻结"); entity.setDataType(param.getDataType());
entity.setPublicDate(param.getPublicDate()); entity.setPublicDate(param.getPublicDate());
if (!ImportHelper.isBlank(param.getFreezeDateStart2())) { if (!ImportHelper.isBlank(param.getFreezeDateStart2())) {
entity.setFreezeDateStart(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.BatchParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditHistoricalLegalPerson; import com.gxwebsoft.credit.entity.CreditHistoricalLegalPerson;
import com.gxwebsoft.credit.param.CreditHistoricalLegalPersonImportParam; import com.gxwebsoft.credit.param.CreditHistoricalLegalPersonImportParam;
import com.gxwebsoft.credit.param.CreditHistoricalLegalPersonParam; import com.gxwebsoft.credit.param.CreditHistoricalLegalPersonParam;
@@ -104,7 +105,7 @@ public class CreditHistoricalLegalPersonController extends BaseController {
@Operation(summary = "删除历史法定代表人") @Operation(summary = "删除历史法定代表人")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditHistoricalLegalPersonService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditHistoricalLegalPerson.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditHistoricalLegalPersonController extends BaseController {
@Operation(summary = "批量删除历史法定代表人") @Operation(summary = "批量删除历史法定代表人")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditHistoricalLegalPersonService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditHistoricalLegalPerson.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditHistoricalLegalPersonController extends BaseController {
CreditHistoricalLegalPerson::getName, CreditHistoricalLegalPerson::getName,
CreditHistoricalLegalPerson::getCompanyId, CreditHistoricalLegalPerson::getCompanyId,
CreditHistoricalLegalPerson::setCompanyId, CreditHistoricalLegalPerson::setCompanyId,
CreditHistoricalLegalPerson::setCompanyName,
CreditHistoricalLegalPerson::getHasData, CreditHistoricalLegalPerson::getHasData,
CreditHistoricalLegalPerson::setHasData, CreditHistoricalLegalPerson::setHasData,
CreditHistoricalLegalPerson::getTenantId, CreditHistoricalLegalPerson::getTenantId,
@@ -210,6 +212,11 @@ public class CreditHistoricalLegalPersonController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null; Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = 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 chunkSize = 500;
final int mpBatchSize = 500; final int mpBatchSize = 500;
@@ -229,6 +236,9 @@ public class CreditHistoricalLegalPersonController extends BaseController {
if (item.getCompanyId() == null && companyId != null) { if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId); item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
} }
if (item.getUserId() == null && currentUserId != null) { if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId); item.setUserId(currentUserId);
@@ -259,86 +269,13 @@ public class CreditHistoricalLegalPersonController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditHistoricalLegalPersonService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> { mpBatchSize,
// 批内一次查库:按 name in (...) 拉取,再按 registerDate 做内存匹配 CreditHistoricalLegalPerson::getName,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -352,85 +289,13 @@ public class CreditHistoricalLegalPersonController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditHistoricalLegalPersonService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> { mpBatchSize,
List<String> names = new ArrayList<>(chunkItems.size()); CreditHistoricalLegalPerson::getName,
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;
},
errorMessages errorMessages
); );
} }

View File

@@ -32,7 +32,6 @@ import java.nio.file.Files;
import java.util.Locale; import java.util.Locale;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -114,7 +113,7 @@ public class CreditJudgmentDebtorController extends BaseController {
@Operation(summary = "删除被执行人") @Operation(summary = "删除被执行人")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditJudgmentDebtorService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditJudgmentDebtor.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -147,7 +146,7 @@ public class CreditJudgmentDebtorController extends BaseController {
@Operation(summary = "批量删除被执行人") @Operation(summary = "批量删除被执行人")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditJudgmentDebtorService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditJudgmentDebtor.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -164,12 +163,14 @@ public class CreditJudgmentDebtorController extends BaseController {
@PostMapping("/company-id/refresh") @PostMapping("/company-id/refresh")
public ApiResult<Map<String, Object>> refreshCompanyIdByCompanyName( public ApiResult<Map<String, Object>> refreshCompanyIdByCompanyName(
@RequestParam(value = "onlyNull", required = false, defaultValue = "true") Boolean onlyNull, @RequestParam(value = "onlyNull", required = false, defaultValue = "true") Boolean onlyNull,
@RequestParam(value = "limit", required = false) Integer limit @RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "topLevelOnly", required = false, defaultValue = "true") Boolean topLevelOnly
) { ) {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( // Match only on "name" column: debtor.name contains a (top-level) CreditCompany name/matchName.
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNameContainedInText(
creditJudgmentDebtorService, creditJudgmentDebtorService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -177,13 +178,22 @@ public class CreditJudgmentDebtorController extends BaseController {
limit, limit,
CreditJudgmentDebtor::getId, CreditJudgmentDebtor::getId,
CreditJudgmentDebtor::setId, CreditJudgmentDebtor::setId,
CreditJudgmentDebtor::getName,
CreditJudgmentDebtor::getCompanyId, CreditJudgmentDebtor::getCompanyId,
CreditJudgmentDebtor::setCompanyId, CreditJudgmentDebtor::setCompanyId,
CreditJudgmentDebtor::getHasData, CreditJudgmentDebtor::getHasData,
CreditJudgmentDebtor::setHasData, CreditJudgmentDebtor::setHasData,
CreditJudgmentDebtor::getTenantId, CreditJudgmentDebtor::getTenantId,
CreditJudgmentDebtor::new CreditJudgmentDebtor::new,
q -> {
if (!Boolean.TRUE.equals(topLevelOnly)) {
return;
}
// "一级企业"parentId=0部分历史数据可能为 NULL兼容处理
q.and(w -> w.eq(com.gxwebsoft.credit.entity.CreditCompany::getParentId, 0)
.or()
.isNull(com.gxwebsoft.credit.entity.CreditCompany::getParentId));
},
CreditJudgmentDebtor::getName
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {
@@ -230,7 +240,7 @@ public class CreditJudgmentDebtorController extends BaseController {
/** /**
* 批量导入历史被执行人(写入被执行人表 credit_judgment_debtor仅解析“历史被执行人”选项卡 * 批量导入历史被执行人(写入被执行人表 credit_judgment_debtor仅解析“历史被执行人”选项卡
* 规则:案号相同则更新;案号不存在则插入;导入文件内案号重复时取最后一条覆盖 * 规则:使用数据库唯一索引约束,重复数据不导入
*/ */
@PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:save')") @PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:save')")
@Operation(summary = "批量导入历史被执行人") @Operation(summary = "批量导入历史被执行人")
@@ -278,7 +288,6 @@ public class CreditJudgmentDebtorController extends BaseController {
example.setName("某某公司"); example.setName("某某公司");
example.setCode("1234567890"); example.setCode("1234567890");
example.setOccurrenceTime("2024-01-10"); example.setOccurrenceTime("2024-01-10");
example.setAmount("100000");
example.setDataStatus("已公开"); example.setDataStatus("已公开");
example.setComments("备注信息"); example.setComments("备注信息");
templateList.add(example); templateList.add(example);
@@ -311,7 +320,9 @@ public class CreditJudgmentDebtorController extends BaseController {
|| isHeaderValue(param.getCode(), "证件号/组织机构代码") || isHeaderValue(param.getCode(), "证件号/组织机构代码")
|| isHeaderValue(param.getOccurrenceTime(), "立案日期") || isHeaderValue(param.getOccurrenceTime(), "立案日期")
|| isHeaderValue(param.getCourtName(), "法院") || isHeaderValue(param.getCourtName(), "法院")
|| isHeaderValue(param.getAmount(), "执行标的(元)") || isHeaderValue(param.getCourtNameQcc(), "执行法院")
|| isHeaderValue(param.getInvolvedAmount(), "涉案金额")
|| isHeaderValue(param.getInvolvedAmountQcc(), "执行标的(元)")
|| isHeaderValue(param.getDataStatus(), "数据状态"); || isHeaderValue(param.getDataStatus(), "数据状态");
} }
@@ -322,17 +333,56 @@ public class CreditJudgmentDebtorController extends BaseController {
return headerText.equals(value.trim()); return headerText.equals(value.trim());
} }
private static boolean hasMeaningfulPartyValue(String value) {
if (ImportHelper.isBlank(value)) {
return false;
}
return !"-".equals(value.trim());
}
private CreditJudgmentDebtor convertImportParamToEntity(CreditJudgmentDebtorImportParam param) { private CreditJudgmentDebtor convertImportParamToEntity(CreditJudgmentDebtorImportParam param) {
CreditJudgmentDebtor entity = new CreditJudgmentDebtor(); CreditJudgmentDebtor entity = new CreditJudgmentDebtor();
entity.setCaseNumber(param.getCaseNumber()); entity.setCaseNumber(param.getCaseNumber());
entity.setName1(param.getName1()); entity.setName1(param.getName1());
String debtorName = ImportHelper.isBlank(param.getName()) ? param.getName1() : param.getName(); String debtorName = ImportHelper.isBlank(param.getName()) ? param.getName1() : param.getName();
if (debtorName != null) {
debtorName = debtorName.trim();
}
entity.setPlaintiffAppellant(param.getPlaintiffAppellant());
entity.setAppellee(param.getAppellee());
entity.setOtherPartiesThirdParty(param.getOtherPartiesThirdParty());
// Some upstream XLS templates store party/company name in "原告/上诉人/被告/第三人" columns.
// When present, use them to populate the debtor "name" for compatibility.
if (hasMeaningfulPartyValue(param.getPlaintiffAppellant())) {
debtorName = param.getPlaintiffAppellant().trim();
} else if (hasMeaningfulPartyValue(param.getAppellee())) {
debtorName = param.getAppellee().trim();
} else if (hasMeaningfulPartyValue(param.getOtherPartiesThirdParty())) {
debtorName = param.getOtherPartiesThirdParty().trim();
}
entity.setName(debtorName); entity.setName(debtorName);
entity.setCode(param.getCode()); entity.setCode(param.getCode());
entity.setOccurrenceTime(param.getOccurrenceTime()); String occurrenceTime = !ImportHelper.isBlank(param.getOccurrenceTime2())
entity.setAmount(param.getAmount()); ? param.getOccurrenceTime2().trim()
entity.setCourtName(param.getCourtName()); : (param.getOccurrenceTime() != null ? param.getOccurrenceTime().trim() : null);
entity.setOccurrenceTime(occurrenceTime);
// 兼容企查查历史被执行人:执行标的(元) / 执行法院
String amount = !ImportHelper.isBlank(param.getInvolvedAmountQcc())
? param.getInvolvedAmountQcc()
: param.getInvolvedAmount();
if (amount != null) {
amount = amount.trim();
}
entity.setAmount(amount);
String courtName = !ImportHelper.isBlank(param.getCourtName())
? param.getCourtName()
: param.getCourtNameQcc();
if (courtName != null) {
courtName = courtName.trim();
}
entity.setCourtName(courtName);
entity.setDataStatus(param.getDataStatus()); entity.setDataStatus(param.getDataStatus());
entity.setComments(param.getComments()); entity.setComments(param.getComments());
@@ -477,10 +527,10 @@ public class CreditJudgmentDebtorController extends BaseController {
Map<String, String> urlByName1 = ExcelImportSupport.readHyperlinksByHeaderKey(excelFile, usedSheetIndex, usedTitleRows, usedHeadRows, "被执行人"); Map<String, String> urlByName1 = ExcelImportSupport.readHyperlinksByHeaderKey(excelFile, usedSheetIndex, usedTitleRows, usedHeadRows, "被执行人");
String prefix = ImportHelper.isBlank(fileLabel) ? "" : "" + fileLabel + ""; String prefix = ImportHelper.isBlank(fileLabel) ? "" : "" + fileLabel + "";
final int chunkSize = 500;
// 同案号多条:以导入文件中“最后一条”为准(视为最新),避免批处理中重复 upsert。 final int mpBatchSize = 500;
LinkedHashMap<String, CreditJudgmentDebtor> latestByCaseNumber = new LinkedHashMap<>(); List<CreditJudgmentDebtor> chunkItems = new ArrayList<>(chunkSize);
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>(); List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
CreditJudgmentDebtorImportParam param = list.get(i); CreditJudgmentDebtorImportParam param = list.get(i);
@@ -510,6 +560,9 @@ public class CreditJudgmentDebtorController extends BaseController {
if (item.getCompanyId() == null && companyId != null) { if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId); item.setCompanyId(companyId);
} }
if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId());
}
if (item.getUserId() == null && currentUserId != null) { if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId); item.setUserId(currentUserId);
} }
@@ -528,35 +581,19 @@ public class CreditJudgmentDebtorController extends BaseController {
// 历史导入的数据统一标记为“失效” // 历史导入的数据统一标记为“失效”
item.setDataStatus("失效"); item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item); chunkItems.add(item);
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) {
successCount += persistHistoryImportChunk(chunkItems, chunkRowNumbers, prefix, mpBatchSize, errorMessages);
chunkItems.clear();
chunkRowNumbers.clear();
}
} catch (Exception e) { } catch (Exception e) {
errorMessages.add(prefix + "" + excelRowNumber + "行:" + e.getMessage()); errorMessages.add(prefix + "" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace(); 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()) { if (!chunkItems.isEmpty()) {
successCount += persistHistoryImportChunk(chunkItems, chunkRowNumbers, prefix, mpBatchSize, errorMessages); successCount += persistHistoryImportChunk(chunkItems, chunkRowNumbers, prefix, mpBatchSize, errorMessages);
} }
@@ -569,101 +606,15 @@ public class CreditJudgmentDebtorController extends BaseController {
String prefix, String prefix,
int mpBatchSize, int mpBatchSize,
List<String> errorMessages) { List<String> errorMessages) {
if (CollectionUtils.isEmpty(items)) { return batchImportSupport.persistInsertOnlyChunk(
return 0; creditJudgmentDebtorService,
} items,
excelRowNumbers,
try { mpBatchSize,
return batchImportSupport.runInNewTx(() -> { CreditJudgmentDebtor::getCaseNumber,
List<String> keys = new ArrayList<>(items.size()); prefix,
for (CreditJudgmentDebtor item : items) { errorMessages
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;
}
} }
private int persistImportChunk(List<CreditJudgmentDebtor> items, private int persistImportChunk(List<CreditJudgmentDebtor> items,
@@ -671,54 +622,15 @@ public class CreditJudgmentDebtorController extends BaseController {
String prefix, String prefix,
int mpBatchSize, int mpBatchSize,
List<String> errorMessages) { List<String> errorMessages) {
if (CollectionUtils.isEmpty(items)) { return batchImportSupport.persistInsertOnlyChunk(
return 0; creditJudgmentDebtorService,
} items,
try { excelRowNumbers,
return batchImportSupport.runInNewTx(() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditJudgmentDebtorService, CreditJudgmentDebtor::getCaseNumber,
items, prefix,
CreditJudgmentDebtor::getId, errorMessages
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;
}
} }
private ImportOutcome importFromZip(MultipartFile zipFile, Integer currentUserId, Integer currentTenantId, Integer companyId) throws Exception { 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.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditJudicialDocumentController extends BaseController {
@Operation(summary = "删除裁判文书司法大数据") @Operation(summary = "删除裁判文书司法大数据")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditJudicialDocumentService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditJudicialDocument.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -138,20 +137,20 @@ public class CreditJudicialDocumentController extends BaseController {
@Operation(summary = "批量删除裁判文书司法大数据") @Operation(summary = "批量删除裁判文书司法大数据")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditJudicialDocumentService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditJudicialDocument.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
} }
/** /**
* 根据企业名称匹配企业并更新 companyId匹配 CreditCompany.name / CreditCompany.matchName * 根据“当事人/第三人”文本中包含的企业名称匹配企业并更新 companyId匹配 CreditCompany.name / CreditCompany.matchName
* *
* <p>默认仅更新 companyId=0 的记录;如需覆盖更新,传 onlyNull=false。</p> * <p>默认仅更新 companyId=0 的记录;如需覆盖更新,传 onlyNull=false。</p>
*/ */
@PreAuthorize("hasAuthority('credit:creditJudicialDocument:update')") @PreAuthorize("hasAuthority('credit:creditJudicialDocument:update')")
@OperationLog @OperationLog
@Operation(summary = "根据企业名称匹配并更新companyId") @Operation(summary = "根据当事人字段包含企业名称匹配并更新companyId")
@PostMapping("/company-id/refresh") @PostMapping("/company-id/refresh")
public ApiResult<Map<String, Object>> refreshCompanyIdByCompanyName( public ApiResult<Map<String, Object>> refreshCompanyIdByCompanyName(
@RequestParam(value = "onlyNull", required = false, defaultValue = "true") Boolean onlyNull, @RequestParam(value = "onlyNull", required = false, defaultValue = "true") Boolean onlyNull,
@@ -160,7 +159,7 @@ public class CreditJudicialDocumentController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNameContainedInText(
creditJudicialDocumentService, creditJudicialDocumentService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -168,13 +167,14 @@ public class CreditJudicialDocumentController extends BaseController {
limit, limit,
CreditJudicialDocument::getId, CreditJudicialDocument::getId,
CreditJudicialDocument::setId, CreditJudicialDocument::setId,
CreditJudicialDocument::getAppellee,
CreditJudicialDocument::getCompanyId, CreditJudicialDocument::getCompanyId,
CreditJudicialDocument::setCompanyId, CreditJudicialDocument::setCompanyId,
CreditJudicialDocument::getHasData, CreditJudicialDocument::getHasData,
CreditJudicialDocument::setHasData, CreditJudicialDocument::setHasData,
CreditJudicialDocument::getTenantId, CreditJudicialDocument::getTenantId,
CreditJudicialDocument::new CreditJudicialDocument::new,
// 需求otherPartiesThirdParty 字段包含企业名称时,回填 companyId
CreditJudicialDocument::getOtherPartiesThirdParty
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {
@@ -270,38 +270,13 @@ public class CreditJudicialDocumentController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditJudicialDocumentService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditJudicialDocumentService, CreditJudicialDocument::getCaseNumber,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -315,38 +290,13 @@ public class CreditJudicialDocumentController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditJudicialDocumentService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditJudicialDocumentService, CreditJudicialDocument::getCaseNumber,
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;
},
errorMessages errorMessages
); );
} }
@@ -366,7 +316,7 @@ public class CreditJudicialDocumentController extends BaseController {
/** /**
* 批量导入历史裁判文书(仅解析“历史裁判文书”选项卡) * 批量导入历史裁判文书(仅解析“历史裁判文书”选项卡)
* 规则:案号相同则覆盖更新recommend++ 记录更新次数);案号不存在则插入。 * 规则:使用数据库唯一索引约束,重复数据不导入。
*/ */
@PreAuthorize("hasAuthority('credit:creditJudicialDocument:save')") @PreAuthorize("hasAuthority('credit:creditJudicialDocument:save')")
@Operation(summary = "批量导入历史裁判文书司法大数据") @Operation(summary = "批量导入历史裁判文书司法大数据")
@@ -400,8 +350,10 @@ public class CreditJudicialDocumentController extends BaseController {
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号"); Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
Map<String, String> urlByTitle = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "文书标题"); Map<String, String> urlByTitle = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "文书标题");
LinkedHashMap<String, CreditJudicialDocument> latestByCaseNumber = new LinkedHashMap<>(); final int chunkSize = 500;
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>(); final int mpBatchSize = 500;
List<CreditJudicialDocument> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
CreditJudicialDocumentImportParam param = list.get(i); CreditJudicialDocumentImportParam param = list.get(i);
@@ -445,121 +397,42 @@ public class CreditJudicialDocumentController extends BaseController {
// 历史导入的数据统一标记为“失效” // 历史导入的数据统一标记为“失效”
item.setDataStatus("失效"); item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item); if (item.getRecommend() == null) {
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber); 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) { } catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage()); errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace(); 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()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditJudicialDocumentService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate( mpBatchSize,
creditJudicialDocumentService, CreditJudicialDocument::getCaseNumber,
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 errorMessages
); );
} }
@@ -621,7 +494,7 @@ public class CreditJudicialDocumentController extends BaseController {
: param.getInvolvedAmount(); : param.getInvolvedAmount();
entity.setTitle(param.getTitle()); entity.setTitle(param.getTitle());
entity.setType(param.getType()); entity.setDocumentType(param.getDocumentType());
entity.setDataStatus(param.getDataStatus()); entity.setDataStatus(param.getDataStatus());
entity.setOtherPartiesThirdParty(param.getOtherPartiesThirdParty()); entity.setOtherPartiesThirdParty(param.getOtherPartiesThirdParty());
entity.setOccurrenceTime(param.getOccurrenceTime()); entity.setOccurrenceTime(param.getOccurrenceTime());

View File

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

View File

@@ -104,7 +104,7 @@ public class CreditMediationController extends BaseController {
@Operation(summary = "删除诉前调解司法大数据") @Operation(summary = "删除诉前调解司法大数据")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditMediationService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditMediation.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +137,7 @@ public class CreditMediationController extends BaseController {
@Operation(summary = "批量删除诉前调解司法大数据") @Operation(summary = "批量删除诉前调解司法大数据")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditMediationService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditMediation.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -159,7 +159,9 @@ public class CreditMediationController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( // Party columns may contain multiple roles/names; match if any company name is contained in the text.
// Priority: 原告/上诉人 > 被告/被上诉人 > 其他当事人/第三人
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNameContainedInText(
creditMediationService, creditMediationService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -167,13 +169,15 @@ public class CreditMediationController extends BaseController {
limit, limit,
CreditMediation::getId, CreditMediation::getId,
CreditMediation::setId, CreditMediation::setId,
CreditMediation::getAppellee,
CreditMediation::getCompanyId, CreditMediation::getCompanyId,
CreditMediation::setCompanyId, CreditMediation::setCompanyId,
CreditMediation::getHasData, CreditMediation::getHasData,
CreditMediation::setHasData, CreditMediation::setHasData,
CreditMediation::getTenantId, CreditMediation::getTenantId,
CreditMediation::new CreditMediation::new,
CreditMediation::getPlaintiffAppellant,
CreditMediation::getAppellee,
CreditMediation::getOtherPartiesThirdParty
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {
@@ -260,38 +264,13 @@ public class CreditMediationController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditMediationService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditMediationService, CreditMediation::getCaseNumber,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -305,38 +284,13 @@ public class CreditMediationController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditMediationService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditMediationService, CreditMediation::getCaseNumber,
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;
},
errorMessages errorMessages
); );
} }
@@ -354,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.BatchParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditNearbyCompany; import com.gxwebsoft.credit.entity.CreditNearbyCompany;
import com.gxwebsoft.credit.param.CreditNearbyCompanyImportParam; import com.gxwebsoft.credit.param.CreditNearbyCompanyImportParam;
import com.gxwebsoft.credit.param.CreditNearbyCompanyParam; import com.gxwebsoft.credit.param.CreditNearbyCompanyParam;
@@ -104,7 +105,7 @@ public class CreditNearbyCompanyController extends BaseController {
@Operation(summary = "删除附近企业") @Operation(summary = "删除附近企业")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditNearbyCompanyService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditNearbyCompany.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditNearbyCompanyController extends BaseController {
@Operation(summary = "批量删除附近企业") @Operation(summary = "批量删除附近企业")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditNearbyCompanyService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditNearbyCompany.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditNearbyCompanyController extends BaseController {
CreditNearbyCompany::getName, CreditNearbyCompany::getName,
CreditNearbyCompany::getCompanyId, CreditNearbyCompany::getCompanyId,
CreditNearbyCompany::setCompanyId, CreditNearbyCompany::setCompanyId,
CreditNearbyCompany::setCompanyName,
CreditNearbyCompany::getHasData, CreditNearbyCompany::getHasData,
CreditNearbyCompany::setHasData, CreditNearbyCompany::setHasData,
CreditNearbyCompany::getTenantId, CreditNearbyCompany::getTenantId,
@@ -213,6 +215,11 @@ public class CreditNearbyCompanyController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCode = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "统一社会信用代码"); Map<String, String> urlByCode = ExcelImportSupport.readHyperlinksByHeaderKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "统一社会信用代码");
Map<String, String> urlByName = 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 次数与事务开销 // 避免逐行写库:按批处理,显著降低 SQL 次数与事务开销
final int chunkSize = 500; final int chunkSize = 500;
@@ -243,6 +250,9 @@ public class CreditNearbyCompanyController extends BaseController {
} }
if (item.getCompanyId() == null && companyId != null) { if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId); item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
} }
if (item.getCompanyId() != null && item.getCompanyId() > 0) { if (item.getCompanyId() != null && item.getCompanyId() > 0) {
touchedCompanyIds.add(item.getCompanyId()); touchedCompanyIds.add(item.getCompanyId());
@@ -308,58 +318,22 @@ public class CreditNearbyCompanyController extends BaseController {
Integer tenantId, Integer tenantId,
int mpBatchSize, int mpBatchSize,
List<String> errorMessages) { List<String> errorMessages) {
return batchImportSupport.persistChunkWithFallback( return batchImportSupport.persistInsertOnlyChunk(
creditNearbyCompanyService,
items, items,
excelRowNumbers, excelRowNumbers,
() -> batchImportSupport.upsertByCodeOrName( mpBatchSize,
creditNearbyCompanyService, it -> {
items, if (it == null) {
CreditNearbyCompany::getId, return null;
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;
} }
String prefix = excelRowNumber > 0 ? ("" + excelRowNumber + "行:") : ""; String code = it.getCode();
errorMessages.add(prefix + "保存失败"); if (code != null && !code.trim().isEmpty()) {
return false; return code;
}
return it.getName();
}, },
"",
errorMessages errorMessages
); );
} }
@@ -444,7 +418,6 @@ public class CreditNearbyCompanyController extends BaseController {
entity.setRegistrationStatus(param.getRegistrationStatus()); entity.setRegistrationStatus(param.getRegistrationStatus());
entity.setLegalPerson(param.getLegalPerson()); entity.setLegalPerson(param.getLegalPerson());
entity.setRegisteredCapital(param.getRegisteredCapital()); entity.setRegisteredCapital(param.getRegisteredCapital());
entity.setPaidinCapital(param.getPaidinCapital());
entity.setEstablishDate(param.getEstablishDate()); entity.setEstablishDate(param.getEstablishDate());
entity.setCode(param.getCode()); entity.setCode(param.getCode());
entity.setAddress(param.getAddress()); entity.setAddress(param.getAddress());
@@ -453,21 +426,35 @@ public class CreditNearbyCompanyController extends BaseController {
entity.setProvince(param.getProvince()); entity.setProvince(param.getProvince());
entity.setCity(param.getCity()); entity.setCity(param.getCity());
entity.setRegion(param.getRegion()); 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.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.setInstitutionType(param.getInstitutionType());
entity.setCompanySize(param.getCompanySize()); 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.setCompanyProfile(param.getCompanyProfile());
entity.setNatureOfBusiness(param.getNatureOfBusiness()); entity.setNatureOfBusiness(param.getNatureOfBusiness());
entity.setComments(param.getComments()); entity.setComments(param.getComments());
entity.setMoreEmail(param.getMoreEmail());
entity.setMoreTel(param.getMoreTel());
return entity; return entity;
} }

View File

@@ -104,7 +104,7 @@ public class CreditPatentController extends BaseController {
@Operation(summary = "删除专利") @Operation(summary = "删除专利")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditPatentService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditPatent.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +137,7 @@ public class CreditPatentController extends BaseController {
@Operation(summary = "批量删除专利") @Operation(summary = "批量删除专利")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditPatentService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditPatent.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -195,8 +195,13 @@ public class CreditPatentController extends BaseController {
Set<Integer> touchedCompanyIds = new HashSet<>(); Set<Integer> touchedCompanyIds = new HashSet<>();
try { try {
ExcelImportSupport.ImportResult<CreditPatentImportParam> importResult = ExcelImportSupport.readAnySheet( // 单企业表通常是多 sheet专利页签名一般为“专利”
file, CreditPatentImportParam.class, this::isEmptyImportRow); 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(); List<CreditPatentImportParam> list = importResult.getData();
int usedTitleRows = importResult.getTitleRows(); int usedTitleRows = importResult.getTitleRows();
int usedHeadRows = importResult.getHeadRows(); int usedHeadRows = importResult.getHeadRows();
@@ -264,38 +269,13 @@ public class CreditPatentController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditPatentService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditPatentService, CreditPatent::getRegisterNo,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -309,38 +289,13 @@ public class CreditPatentController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditPatentService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditPatentService, CreditPatent::getRegisterNo,
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;
},
errorMessages errorMessages
); );
} }
@@ -395,7 +350,16 @@ public class CreditPatentController extends BaseController {
if (isImportHeaderRow(param)) { if (isImportHeaderRow(param)) {
return true; 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) { 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.BatchParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditRiskRelation; import com.gxwebsoft.credit.entity.CreditRiskRelation;
import com.gxwebsoft.credit.param.CreditRiskRelationImportParam; import com.gxwebsoft.credit.param.CreditRiskRelationImportParam;
import com.gxwebsoft.credit.param.CreditRiskRelationParam; import com.gxwebsoft.credit.param.CreditRiskRelationParam;
@@ -104,7 +105,7 @@ public class CreditRiskRelationController extends BaseController {
@Operation(summary = "删除风险关系表") @Operation(summary = "删除风险关系表")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditRiskRelationService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditRiskRelation.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditRiskRelationController extends BaseController {
@Operation(summary = "批量删除风险关系表") @Operation(summary = "批量删除风险关系表")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditRiskRelationService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditRiskRelation.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditRiskRelationController extends BaseController {
CreditRiskRelation::getMainBodyName, CreditRiskRelation::getMainBodyName,
CreditRiskRelation::getCompanyId, CreditRiskRelation::getCompanyId,
CreditRiskRelation::setCompanyId, CreditRiskRelation::setCompanyId,
CreditRiskRelation::setCompanyName,
CreditRiskRelation::getHasData, CreditRiskRelation::getHasData,
CreditRiskRelation::setHasData, CreditRiskRelation::setHasData,
CreditRiskRelation::getTenantId, CreditRiskRelation::getTenantId,
@@ -209,6 +211,11 @@ public class CreditRiskRelationController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null; Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : 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 chunkSize = 500;
final int mpBatchSize = 500; final int mpBatchSize = 500;
@@ -222,6 +229,9 @@ public class CreditRiskRelationController extends BaseController {
if (item.getCompanyId() == null && companyId != null) { if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId); item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
} }
if (item.getUserId() == null && currentUserId != null) { if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId); item.setUserId(currentUserId);
@@ -252,38 +262,13 @@ public class CreditRiskRelationController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditRiskRelationService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditRiskRelationService, CreditRiskRelation::getMainBodyName,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -297,38 +282,13 @@ public class CreditRiskRelationController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditRiskRelationService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditRiskRelationService, CreditRiskRelation::getMainBodyName,
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;
},
errorMessages 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.BatchParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditSupplier; import com.gxwebsoft.credit.entity.CreditSupplier;
import com.gxwebsoft.credit.param.CreditSupplierImportParam; import com.gxwebsoft.credit.param.CreditSupplierImportParam;
import com.gxwebsoft.credit.param.CreditSupplierParam; import com.gxwebsoft.credit.param.CreditSupplierParam;
@@ -104,7 +105,7 @@ public class CreditSupplierController extends BaseController {
@Operation(summary = "删除供应商") @Operation(summary = "删除供应商")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditSupplierService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditSupplier.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditSupplierController extends BaseController {
@Operation(summary = "批量删除供应商") @Operation(summary = "批量删除供应商")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditSupplierService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditSupplier.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditSupplierController extends BaseController {
CreditSupplier::getSupplier, CreditSupplier::getSupplier,
CreditSupplier::getCompanyId, CreditSupplier::getCompanyId,
CreditSupplier::setCompanyId, CreditSupplier::setCompanyId,
CreditSupplier::setCompanyName,
CreditSupplier::getHasData, CreditSupplier::getHasData,
CreditSupplier::setHasData, CreditSupplier::setHasData,
CreditSupplier::getTenantId, CreditSupplier::getTenantId,
@@ -211,6 +213,11 @@ public class CreditSupplierController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null; Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlBySupplier = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "供应商"); 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 chunkSize = 500;
final int mpBatchSize = 500; final int mpBatchSize = 500;
@@ -230,6 +237,9 @@ public class CreditSupplierController extends BaseController {
if (item.getCompanyId() == null && companyId != null) { if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId); item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
} }
if (item.getUserId() == null && currentUserId != null) { if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId); item.setUserId(currentUserId);
@@ -260,38 +270,13 @@ public class CreditSupplierController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditSupplierService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditSupplierService, CreditSupplier::getSupplier,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -305,38 +290,13 @@ public class CreditSupplierController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditSupplierService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditSupplierService, CreditSupplier::getSupplier,
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;
},
errorMessages 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.BatchParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.entity.CreditSuspectedRelationship; import com.gxwebsoft.credit.entity.CreditSuspectedRelationship;
import com.gxwebsoft.credit.param.CreditSuspectedRelationshipImportParam; import com.gxwebsoft.credit.param.CreditSuspectedRelationshipImportParam;
import com.gxwebsoft.credit.param.CreditSuspectedRelationshipParam; import com.gxwebsoft.credit.param.CreditSuspectedRelationshipParam;
@@ -104,7 +105,7 @@ public class CreditSuspectedRelationshipController extends BaseController {
@Operation(summary = "删除疑似关系") @Operation(summary = "删除疑似关系")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditSuspectedRelationshipService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditSuspectedRelationship.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -137,7 +138,7 @@ public class CreditSuspectedRelationshipController extends BaseController {
@Operation(summary = "批量删除疑似关系") @Operation(summary = "批量删除疑似关系")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditSuspectedRelationshipService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditSuspectedRelationship.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -170,6 +171,7 @@ public class CreditSuspectedRelationshipController extends BaseController {
CreditSuspectedRelationship::getName, CreditSuspectedRelationship::getName,
CreditSuspectedRelationship::getCompanyId, CreditSuspectedRelationship::getCompanyId,
CreditSuspectedRelationship::setCompanyId, CreditSuspectedRelationship::setCompanyId,
CreditSuspectedRelationship::setCompanyName,
CreditSuspectedRelationship::getHasData, CreditSuspectedRelationship::getHasData,
CreditSuspectedRelationship::setHasData, CreditSuspectedRelationship::setHasData,
CreditSuspectedRelationship::getTenantId, CreditSuspectedRelationship::getTenantId,
@@ -210,6 +212,11 @@ public class CreditSuspectedRelationshipController extends BaseController {
Integer currentUserId = loginUser != null ? loginUser.getUserId() : null; Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByName = 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 chunkSize = 500;
final int mpBatchSize = 500; final int mpBatchSize = 500;
@@ -229,6 +236,9 @@ public class CreditSuspectedRelationshipController extends BaseController {
if (item.getCompanyId() == null && companyId != null) { if (item.getCompanyId() == null && companyId != null) {
item.setCompanyId(companyId); item.setCompanyId(companyId);
if (ImportHelper.isBlank(item.getCompanyName()) && !ImportHelper.isBlank(fixedCompanyName)) {
item.setCompanyName(fixedCompanyName);
}
} }
if (item.getUserId() == null && currentUserId != null) { if (item.getUserId() == null && currentUserId != null) {
item.setUserId(currentUserId); item.setUserId(currentUserId);
@@ -263,99 +273,29 @@ public class CreditSuspectedRelationshipController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditSuspectedRelationshipService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> { mpBatchSize,
List<String> names = new ArrayList<>(chunkItems.size()); it -> {
List<String> relatedParties = new ArrayList<>(chunkItems.size()); if (it == null) {
for (CreditSuspectedRelationship it : chunkItems) { return null;
if (it == null) {
continue;
}
if (!ImportHelper.isBlank(it.getName())) {
names.add(it.getName().trim());
}
if (!ImportHelper.isBlank(it.getRelatedParty())) {
relatedParties.add(it.getRelatedParty().trim());
}
} }
String n = it.getName();
List<CreditSuspectedRelationship> existingList = (names.isEmpty() || relatedParties.isEmpty()) String r = it.getRelatedParty();
? new ArrayList<>() if (n != null) {
: creditSuspectedRelationshipService.lambdaQuery() n = n.trim();
.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);
}
} }
if (r != null) {
List<CreditSuspectedRelationship> updates = new ArrayList<>(); r = r.trim();
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 (!updates.isEmpty()) { if (n != null && !n.isEmpty() && r != null && !r.isEmpty()) {
creditSuspectedRelationshipService.updateBatchById(updates, mpBatchSize); return n + "->" + r;
} }
if (!inserts.isEmpty()) { return n;
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;
}, },
"",
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -369,99 +309,29 @@ public class CreditSuspectedRelationshipController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditSuspectedRelationshipService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> { mpBatchSize,
List<String> names = new ArrayList<>(chunkItems.size()); it -> {
List<String> relatedParties = new ArrayList<>(chunkItems.size()); if (it == null) {
for (CreditSuspectedRelationship it : chunkItems) { return null;
if (it == null) {
continue;
}
if (!ImportHelper.isBlank(it.getName())) {
names.add(it.getName().trim());
}
if (!ImportHelper.isBlank(it.getRelatedParty())) {
relatedParties.add(it.getRelatedParty().trim());
}
} }
String n = it.getName();
List<CreditSuspectedRelationship> existingList = (names.isEmpty() || relatedParties.isEmpty()) String r = it.getRelatedParty();
? new ArrayList<>() if (n != null) {
: creditSuspectedRelationshipService.lambdaQuery() n = n.trim();
.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);
}
} }
if (r != null) {
List<CreditSuspectedRelationship> updates = new ArrayList<>(); r = r.trim();
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 (!updates.isEmpty()) { if (n != null && !n.isEmpty() && r != null && !r.isEmpty()) {
creditSuspectedRelationshipService.updateBatchById(updates, mpBatchSize); return n + "->" + r;
} }
if (!inserts.isEmpty()) { return n;
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;
}, },
"",
errorMessages errorMessages
); );
} }

View File

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

View File

@@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -105,7 +104,7 @@ public class CreditXgxfController extends BaseController {
@Operation(summary = "删除限制高消费") @Operation(summary = "删除限制高消费")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (creditXgxfService.removeById(id)) { if (batchImportSupport.hardRemoveById(CreditXgxf.class, id)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -138,7 +137,7 @@ public class CreditXgxfController extends BaseController {
@Operation(summary = "批量删除限制高消费") @Operation(summary = "批量删除限制高消费")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (creditXgxfService.removeByIds(ids)) { if (batchImportSupport.hardRemoveByIds(CreditXgxf.class, ids)) {
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -160,7 +159,9 @@ public class CreditXgxfController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName( // Party columns may contain multiple roles/names; match if any company name is contained in the text.
// Priority: 原告/上诉人 > 被告/被上诉人 > 其他当事人/第三人
BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyNameContainedInText(
creditXgxfService, creditXgxfService,
creditCompanyService, creditCompanyService,
currentTenantId, currentTenantId,
@@ -168,13 +169,15 @@ public class CreditXgxfController extends BaseController {
limit, limit,
CreditXgxf::getId, CreditXgxf::getId,
CreditXgxf::setId, CreditXgxf::setId,
CreditXgxf::getDataType,
CreditXgxf::getCompanyId, CreditXgxf::getCompanyId,
CreditXgxf::setCompanyId, CreditXgxf::setCompanyId,
CreditXgxf::getHasData, CreditXgxf::getHasData,
CreditXgxf::setHasData, CreditXgxf::setHasData,
CreditXgxf::getTenantId, CreditXgxf::getTenantId,
CreditXgxf::new CreditXgxf::new,
CreditXgxf::getPlaintiffAppellant,
CreditXgxf::getAppellee,
CreditXgxf::getOtherPartiesThirdParty
); );
if (!stats.anyDataRead) { if (!stats.anyDataRead) {
@@ -265,38 +268,13 @@ public class CreditXgxfController extends BaseController {
chunkItems.add(item); chunkItems.add(item);
chunkRowNumbers.add(excelRowNumber); chunkRowNumbers.add(excelRowNumber);
if (chunkItems.size() >= chunkSize) { if (chunkItems.size() >= chunkSize) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditXgxfService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditXgxfService, CreditXgxf::getCaseNumber,
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;
},
errorMessages errorMessages
); );
chunkItems.clear(); chunkItems.clear();
@@ -310,38 +288,13 @@ public class CreditXgxfController extends BaseController {
} }
if (!chunkItems.isEmpty()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditXgxfService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKey( mpBatchSize,
creditXgxfService, CreditXgxf::getCaseNumber,
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;
},
errorMessages errorMessages
); );
} }
@@ -361,7 +314,7 @@ public class CreditXgxfController extends BaseController {
/** /**
* 批量导入历史限制高消费(仅解析“历史限制高消费”选项卡) * 批量导入历史限制高消费(仅解析“历史限制高消费”选项卡)
* 规则:案号相同则覆盖更新recommend++ 记录更新次数);案号不存在则插入。 * 规则:使用数据库唯一索引约束,重复数据不导入。
*/ */
@PreAuthorize("hasAuthority('credit:creditXgxf:save')") @PreAuthorize("hasAuthority('credit:creditXgxf:save')")
@Operation(summary = "批量导入历史限制高消费司法大数据") @Operation(summary = "批量导入历史限制高消费司法大数据")
@@ -394,8 +347,10 @@ public class CreditXgxfController extends BaseController {
Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null; Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;
Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号"); Map<String, String> urlByCaseNumber = ExcelImportSupport.readUrlByKey(file, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
LinkedHashMap<String, CreditXgxf> latestByCaseNumber = new LinkedHashMap<>(); final int chunkSize = 500;
LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>(); final int mpBatchSize = 500;
List<CreditXgxf> chunkItems = new ArrayList<>(chunkSize);
List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
CreditXgxfImportParam param = list.get(i); CreditXgxfImportParam param = list.get(i);
@@ -433,121 +388,42 @@ public class CreditXgxfController extends BaseController {
// 历史导入的数据统一标记为“失效” // 历史导入的数据统一标记为“失效”
item.setDataStatus("失效"); item.setDataStatus("失效");
latestByCaseNumber.put(item.getCaseNumber(), item); if (item.getRecommend() == null) {
latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber); 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) { } catch (Exception e) {
errorMessages.add("" + excelRowNumber + "行:" + e.getMessage()); errorMessages.add("" + excelRowNumber + "行:" + e.getMessage());
e.printStackTrace(); 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()) { if (!chunkItems.isEmpty()) {
successCount += batchImportSupport.persistChunkWithFallback( successCount += batchImportSupport.persistInsertOnlyChunk(
creditXgxfService,
chunkItems, chunkItems,
chunkRowNumbers, chunkRowNumbers,
() -> batchImportSupport.upsertBySingleKeyAndIncrementCounterOnUpdate( mpBatchSize,
creditXgxfService, CreditXgxf::getCaseNumber,
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 errorMessages
); );
} }
@@ -602,27 +478,29 @@ public class CreditXgxfController extends BaseController {
private CreditXgxf convertImportParamToEntity(CreditXgxfImportParam param) { private CreditXgxf convertImportParamToEntity(CreditXgxfImportParam param) {
CreditXgxf entity = new CreditXgxf(); 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.setCaseNumber(param.getCaseNumber());
entity.setType(param.getType()); entity.setType(param.getType());
entity.setDataType(param.getDataType()); entity.setDataType(param.getDataType());
entity.setPlaintiffUser(param.getPlaintiffUser()); entity.setPlaintiffAppellant(plaintiffAppellant);
entity.setDefendantUser(param.getDefendantUser()); entity.setAppellee(appellee);
entity.setOtherPartiesThirdParty(param.getOtherPartiesThirdParty()); entity.setOtherPartiesThirdParty(param.getOtherPartiesThirdParty());
entity.setPlaintiffAppellant(param.getPlaintiffAppellant());
entity.setDataStatus(param.getDataStatus()); entity.setDataStatus(param.getDataStatus());
entity.setAppellee(param.getAppellee());
// 兼容不同模板字段:如果 *2 有值则以 *2 为准写入主字段
entity.setInvolvedAmount(!ImportHelper.isBlank(param.getInvolvedAmount2())
? param.getInvolvedAmount2()
: param.getInvolvedAmount());
entity.setOccurrenceTime(!ImportHelper.isBlank(param.getOccurrenceTime2())
? param.getOccurrenceTime2()
: param.getOccurrenceTime());
entity.setCourtName(!ImportHelper.isBlank(param.getCourtName2())
? param.getCourtName2()
: param.getCourtName());
entity.setInvolvedAmount(param.getInvolvedAmount());
entity.setOccurrenceTime(param.getOccurrenceTime());
entity.setCourtName(courtName);
entity.setReleaseDate(param.getReleaseDate()); entity.setReleaseDate(param.getReleaseDate());
entity.setComments(param.getComments()); 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.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil; 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.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams; 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.Cell;
import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.DataFormatter;
@@ -18,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -277,7 +280,7 @@ public class ExcelImportSupport {
if (workbook.getNumberOfSheets() > sheetIndex) { if (workbook.getNumberOfSheets() > sheetIndex) {
Sheet sheet = workbook.getSheetAt(sheetIndex); Sheet sheet = workbook.getSheetAt(sheetIndex);
if (sheet != null) { if (sheet != null) {
normalizeHeaderCells(sheet, titleRows, headRows); normalizeHeaderCells(sheet, titleRows, headRows, clazz);
} }
} }
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { 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) { if (sheet == null || headRows <= 0) {
return; return;
} }
Map<String, String> expectedHeadersByKey = buildExpectedHeaderKeyMap(clazz);
if (expectedHeadersByKey.isEmpty()) {
return;
}
int headerStart = Math.max(titleRows, 0); int headerStart = Math.max(titleRows, 0);
int headerEnd = headerStart + headRows - 1; int headerEnd = headerStart + headRows - 1;
for (int r = headerStart; r <= headerEnd; r++) { for (int r = headerStart; r <= headerEnd; r++) {
@@ -317,21 +324,99 @@ public class ExcelImportSupport {
continue; continue;
} }
String normalized = normalizeHeaderText(text); String canonical = findCanonicalHeader(text, expectedHeadersByKey);
if (normalized != null && !normalized.equals(text)) { if (canonical != null && !canonical.equals(text)) {
cell.setCellValue(normalized); cell.setCellValue(canonical);
} }
} }
} }
} }
private static String normalizeHeaderText(String text) { private static Map<String, String> buildExpectedHeaderKeyMap(Class<?> clazz) {
if (text == null) { 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; return null;
} }
// Remove common invisible whitespace characters, including full-width space. String key = normalizeHeaderKey(rawHeaderText);
return text 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("", "[")
.replace("", "]")
// remove common invisible whitespace characters, including full-width space.
.replace(" ", "") .replace(" ", "")
.replace("\t", "") .replace("\t", "")
.replace("\r", "") .replace("\r", "")
@@ -339,6 +424,34 @@ public class ExcelImportSupport {
.replace("\u00A0", "") .replace("\u00A0", "")
.replace(" ", "") .replace(" ", "")
.trim(); .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) { 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) { 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 firstHeaderRow = Math.max(0, titleRows);
int lastHeaderRow = Math.max(0, titleRows + headRows - 1); int lastHeaderRow = Math.max(0, titleRows + headRows - 1);
for (int r = firstHeaderRow; r <= lastHeaderRow; r++) { for (int r = firstHeaderRow; r <= lastHeaderRow; r++) {
@@ -565,17 +682,10 @@ public class ExcelImportSupport {
if (cell == null) { if (cell == null) {
continue; continue;
} }
if (cell.getCellType() == CellType.STRING) { DataFormatter formatter = new DataFormatter();
String value = cell.getStringCellValue(); String value = formatter.formatCellValue(cell);
if (headerName.equals(value != null ? value.trim() : null)) { if (targetKey.equals(normalizeHeaderKey(value))) {
return c; return c;
}
} else {
DataFormatter formatter = new DataFormatter();
String value = formatter.formatCellValue(cell);
if (headerName.equals(value != null ? value.trim() : null)) {
return c;
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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