Compare commits

...

40 Commits

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

View File

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

View File

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

View File

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

View File

@@ -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请求
// access_token 失效/过期时自动刷新并重试一次
for (int attempt = 0; attempt < 2; attempt++) {
String accessToken = (attempt == 0) ? getAccessToken(false) : getAccessToken(true);
String apiUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken;
String post = HttpUtil.post(apiUrl, JSON.toJSONString(paramMap)); String post = HttpUtil.post(apiUrl, JSON.toJSONString(paramMap));
JSONObject json = JSON.parseObject(post); JSONObject json = JSON.parseObject(post);
if (json.get("errcode").equals(0)) {
Integer errcode = json.getInteger("errcode");
if (errcode != null && errcode.equals(0)) {
JSONObject phoneInfo = JSON.parseObject(json.getString("phone_info")); JSONObject phoneInfo = JSON.parseObject(json.getString("phone_info"));
// 微信用户的手机号码 return phoneInfo.getString("phoneNumber");
final String phoneNumber = phoneInfo.getString("phoneNumber"); }
// 验证手机号码
// if (userParam.getNotVerifyPhone() == null && !Validator.isMobile(phoneNumber)) { if (errcode != null && (errcode == 40001 || errcode == 42001 || errcode == 40014)) {
// String key = ACCESS_TOKEN_KEY.concat(":").concat(getTenantId().toString()); continue;
// redisTemplate.delete(key); }
// throw new BusinessException("手机号码格式不正确"); return null;
// }
return phoneNumber;
} }
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,8 +842,8 @@ 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");
if (errcode != null && errcode != 0) {
System.err.println("微信API错误 - errcode: " + errcode + ", errmsg: " + errmsg); 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) {
@@ -810,13 +852,15 @@ public class WxLoginController extends BaseController {
throw new RuntimeException("微信API调用失败: " + errmsg + " (errcode: " + errcode + ")"); throw new RuntimeException("微信API调用失败: " + errmsg + " (errcode: " + errcode + ")");
} }
} }
}
if (json.containsKey("access_token")) { if (json.containsKey("access_token")) {
String accessToken = json.getString("access_token"); String accessToken = json.getString("access_token");
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

@@ -355,6 +355,7 @@ 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<>(); HashMap<Integer, String> companyNameById = new HashMap<>();
LinkedHashSet<String> nameSet = new LinkedHashSet<>(); LinkedHashSet<String> nameSet = new LinkedHashSet<>();
for (T row : tenantRows) { for (T row : tenantRows) {
@@ -385,8 +386,12 @@ public class BatchImportSupport {
if (c == null || c.getId() == null) { if (c == null || c.getId() == null) {
continue; continue;
} }
if (c.getName() != null && !c.getName().trim().isEmpty()) { String displayName = c.getMatchName();
companyNameById.putIfAbsent(c.getId(), c.getName().trim()); 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());

View File

@@ -195,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();

View File

@@ -309,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

@@ -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;
@@ -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);

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;
@@ -167,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,
@@ -208,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;
@@ -229,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);

View File

@@ -310,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;
@@ -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);

View File

@@ -539,7 +539,7 @@ public class CreditGqdjController extends BaseController {
item.setDeleted(0); item.setDeleted(0);
} }
// 历史导入的数据统一标记为“失效” // 历史导入的数据统一标记为“失效”
item.setDataStatus("失效"); item.setDataType("失效");
if (item.getRecommend() == null) { if (item.getRecommend() == null) {
item.setRecommend(0); item.setRecommend(0);
@@ -667,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

@@ -308,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

@@ -418,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());
@@ -427,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

@@ -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;
@@ -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);

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;
@@ -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);

View File

@@ -8,6 +8,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.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;
@@ -176,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,
@@ -216,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;
@@ -233,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());
@@ -432,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

@@ -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 = "是否有数据")

View File

@@ -49,6 +49,9 @@ public class CreditHistoricalLegalPerson 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

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

View File

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

View File

@@ -50,8 +50,10 @@ public class CreditRiskRelation 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

@@ -51,8 +51,10 @@ public class CreditSupplier 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

@@ -64,6 +64,9 @@ public class CreditSuspectedRelationship 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

@@ -39,6 +39,9 @@ public class CreditUser implements Serializable {
@Schema(description = "类型, 0普通用户, 1招投标") @Schema(description = "类型, 0普通用户, 1招投标")
private Integer type; private Integer type;
@Schema(description = "原告/上诉人")
private String plaintiffAppellant;
@Schema(description = "企业角色") @Schema(description = "企业角色")
private String role; private String role;
@@ -78,8 +81,10 @@ public class CreditUser 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

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

View File

@@ -23,6 +23,9 @@
<if test="param.type != null"> <if test="param.type != null">
AND a.type = #{param.type} AND a.type = #{param.type}
</if> </if>
<if test="param.isCustomer != null">
AND a.is_customer = #{param.isCustomer}
</if>
<if test="param.parentId != null"> <if test="param.parentId != null">
AND a.parent_id = #{param.parentId} AND a.parent_id = #{param.parentId}
</if> </if>

View File

@@ -0,0 +1,219 @@
<?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.credit.mapper.CreditMpCustomerMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*, u.nickname AS nickname, u.avatar AS avatar, u.phone AS phone, u.real_name AS realName
FROM credit_mp_customer a
LEFT JOIN gxwebsoft_core.sys_user u ON u.user_id = a.user_id
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.toUser != null">
AND a.to_user LIKE CONCAT('%', #{param.toUser}, '%')
</if>
<if test="param.price != null">
AND a.price LIKE CONCAT('%', #{param.price}, '%')
</if>
<if test="param.years != null">
AND a.years LIKE CONCAT('%', #{param.years}, '%')
</if>
<if test="param.url != null">
AND a.url LIKE CONCAT('%', #{param.url}, '%')
</if>
<if test="param.statusTxt != null">
AND a.status_txt LIKE CONCAT('%', #{param.statusTxt}, '%')
</if>
<if test="param.companyId != null">
AND a.company_id = #{param.companyId}
</if>
<if test="param.province != null">
AND a.province LIKE CONCAT('%', #{param.province}, '%')
</if>
<if test="param.city != null">
AND a.city LIKE CONCAT('%', #{param.city}, '%')
</if>
<if test="param.region != null">
AND a.region LIKE CONCAT('%', #{param.region}, '%')
</if>
<if test="param.files != null">
AND a.files LIKE CONCAT('%', #{param.files}, '%')
</if>
<if test="param.hasData != null">
AND a.has_data = #{param.hasData}
</if>
<if test="param.comments != null">
AND a.comments LIKE CONCAT('%', #{param.comments}, '%')
</if>
<if test="param.recommend != null">
AND a.recommend = #{param.recommend}
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.step != null">
AND a.step = #{param.step}
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
OR a.to_user LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.credit.entity.CreditMpCustomer">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.credit.entity.CreditMpCustomer">
<include refid="selectSql"></include>
</select>
<!-- 获取待审核的跟进步骤列表 -->
<select id="selectPendingApprovalSteps" resultType="com.gxwebsoft.credit.vo.PendingApprovalStepVO">
SELECT
c.id as customerId,
c.to_user as customerName,
1 as step,
'案件受理' as stepTitle,
c.follow_step1_submitted_at as submittedAt,
u.real_name as submittedBy,
c.comments as content
FROM credit_mp_customer c
LEFT JOIN sys_user u ON c.user_id = u.user_id
WHERE c.follow_step1_submitted = 1
AND c.follow_step1_approved = 0
AND c.deleted = 0
UNION ALL
SELECT
c.id as customerId,
c.to_user as customerName,
2 as step,
'材料准备' as stepTitle,
c.follow_step2_submitted_at as submittedAt,
u.real_name as submittedBy,
c.comments as content
FROM credit_mp_customer c
LEFT JOIN sys_user u ON c.user_id = u.user_id
WHERE c.follow_step2_submitted = 1
AND c.follow_step2_approved = 0
AND c.deleted = 0
UNION ALL
SELECT
c.id as customerId,
c.to_user as customerName,
3 as step,
'案件办理' as stepTitle,
c.follow_step3_submitted_at as submittedAt,
u.real_name as submittedBy,
c.comments as content
FROM credit_mp_customer c
LEFT JOIN sys_user u ON c.user_id = u.user_id
WHERE c.follow_step3_submitted = 1
AND c.follow_step3_approved = 0
AND c.deleted = 0
UNION ALL
SELECT
c.id as customerId,
c.to_user as customerName,
4 as step,
'送达签收' as stepTitle,
c.follow_step4_submitted_at as submittedAt,
u.real_name as submittedBy,
c.comments as content
FROM credit_mp_customer c
LEFT JOIN sys_user u ON c.user_id = u.user_id
WHERE c.follow_step4_submitted = 1
AND c.follow_step4_approved = 0
AND c.deleted = 0
UNION ALL
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="param.step != null">
HAVING step = #{param.step}
</if>
<if test="param.customerId != null">
HAVING customerId = #{param.customerId}
</if>
<if test="param.userId != null">
HAVING customerId IN (SELECT id FROM credit_mp_customer WHERE user_id = #{param.userId})
</if>
ORDER BY submittedAt DESC
</select>
</mapper>

View File

@@ -0,0 +1,24 @@
package com.gxwebsoft.credit.param;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 批量跟进步骤审核DTO
*
* @author 科技小王子
* @since 2026-03-22
*/
@Data
@Schema(name = "BatchFollowStepApprovalDTO对象", description = "批量跟进步骤审核DTO")
public class BatchFollowStepApprovalDTO {
@Schema(description = "审核列表", required = true)
@NotNull(message = "审核列表不能为空")
@Valid
private List<FollowStepApprovalDTO> approvals;
}

View File

@@ -38,6 +38,10 @@ public class CreditCompanyParam extends BaseParam {
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer type; private Integer type;
@Schema(description = "是否客户")
@QueryField(type = QueryType.EQ)
private Integer isCustomer;
@Schema(description = "上级id, 0是顶级") @Schema(description = "上级id, 0是顶级")
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer parentId; private Integer parentId;

View File

@@ -31,10 +31,10 @@ public class CreditCourtAnnouncementImportParam implements Serializable {
@Excel(name = "数据类型") @Excel(name = "数据类型")
private String dataType2; private String dataType2;
@Excel(name = "公告") @Excel(name = "原告/上诉")
private String plaintiffAppellant; private String plaintiffAppellant;
@Excel(name = "原告/上诉人") @Excel(name = "原告/上诉人2")
private String plaintiffAppellant2; private String plaintiffAppellant2;
@Excel(name = "被告/被上诉人") @Excel(name = "被告/被上诉人")

View File

@@ -1,6 +1,7 @@
package com.gxwebsoft.credit.param; package com.gxwebsoft.credit.param;
import cn.afterturn.easypoi.excel.annotation.Excel; import cn.afterturn.easypoi.excel.annotation.Excel;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -40,14 +41,14 @@ public class CreditGqdjImportParam implements Serializable {
@Excel(name = "执行法院") @Excel(name = "执行法院")
private String courtName; private String courtName;
@Excel(name = "类型") @Excel(name = "数据状态")
private String dataType; private String dataType;
@Excel(name = "状态") @Excel(name = "状态")
private String dataStatus; private String dataStatus;
// Some upstream sources use "数据状态" as the status column. // Some upstream sources use "数据状态" as the status column.
@Excel(name = "数据状态") @Excel(name = "数据状态2")
private String dataStatus2; private String dataStatus2;
@Excel(name = "冻结日期自") @Excel(name = "冻结日期自")

View File

@@ -0,0 +1,91 @@
package com.gxwebsoft.credit.param;
import java.math.BigDecimal;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 小程序端客户查询参数
*
* @author 科技小王子
* @since 2026-03-16 20:59:17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "CreditMpCustomerParam对象", description = "小程序端客户查询参数")
public class CreditMpCustomerParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "ID")
@QueryField(type = QueryType.EQ)
private Integer id;
@Schema(description = "拖欠方")
private String toUser;
@Schema(description = "拖欠金额")
private String price;
@Schema(description = "拖欠年数")
private String years;
@Schema(description = "链接")
private String url;
@Schema(description = "状态")
private String statusTxt;
@Schema(description = "企业ID")
@QueryField(type = QueryType.EQ)
private Integer companyId;
@Schema(description = "所在省份")
private String province;
@Schema(description = "所在城市")
private String city;
@Schema(description = "所在辖区")
private String region;
@Schema(description = "文件路径")
private String files;
@Schema(description = "是否有数据")
@QueryField(type = QueryType.EQ)
private Boolean hasData;
@Schema(description = "步骤, 0未受理1已受理2材料提交3合同签订4执行回款5完结")
@QueryField(type = QueryType.EQ)
private Integer step;
@Schema(description = "备注")
private String comments;
@Schema(description = "是否推荐")
@QueryField(type = QueryType.EQ)
private Integer recommend;
@Schema(description = "排序(数字越小越靠前)")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@Schema(description = "状态, 0正常, 1冻结")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@QueryField(type = QueryType.EQ)
private Integer deleted;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
}

View File

@@ -1,6 +1,7 @@
package com.gxwebsoft.credit.param; package com.gxwebsoft.credit.param;
import cn.afterturn.easypoi.excel.annotation.Excel; import cn.afterturn.easypoi.excel.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@@ -42,6 +43,9 @@ public class CreditNearbyCompanyImportParam implements Serializable {
@Excel(name = "邮箱") @Excel(name = "邮箱")
private String email; private String email;
@Excel(name = "更多邮箱")
private String moreEmail;
@Excel(name = "所属省份") @Excel(name = "所属省份")
private String province; private String province;
@@ -51,14 +55,74 @@ public class CreditNearbyCompanyImportParam implements Serializable {
@Excel(name = "所属区县") @Excel(name = "所属区县")
private String region; private String region;
@Excel(name = "纳税人识别号")
private String taxpayerCode;
@Excel(name = "注册号")
private String registrationNumber;
@Excel(name = "组织机构代码")
private String organizationalCode;
@Excel(name = "参保人数")
private String numberOfInsuredPersons;
@Excel(name = "参保人数所属年报")
private String annualReport;
@Excel(name = "营业期限")
private String businessTerm;
@Excel(name = "国标行业门类")
private String nationalStandardIndustryCategories;
@Excel(name = "国标行业大类")
private String nationalStandardIndustryCategories2;
@Excel(name = "国标行业中类")
private String nationalStandardIndustryCategories3;
@Excel(name = "国标行业小类")
private String nationalStandardIndustryCategories4;
@Excel(name = "曾用名")
private String formerName;
@Excel(name = "英文名")
private String englishName;
@Excel(name = "官网网址") @Excel(name = "官网网址")
private String domain; private String domain;
@Excel(name = "企业(机构)类型") @Excel(name = "通信地址")
private String institutionType; private String mailingAddress;
@Excel(name = "企业规模") @Excel(name = "通信地址邮编")
private String companySize; private String mailingEmail;
@Excel(name = "注册地址邮编")
private String postalCode;
@Excel(name = "电话")
private String tel;
@Excel(name = "更多电话")
private String moreTel;
@Excel(name = "企查查行业门类")
private String nationalStandardIndustryCategories5;
@Excel(name = "企查查行业大类")
private String nationalStandardIndustryCategories6;
@Excel(name = "企查查行业中类")
private String nationalStandardIndustryCategories7;
@Excel(name = "企查查行业小类")
private String nationalStandardIndustryCategories8;
@Excel(name = "类型")
private Integer type;
@Excel(name = "登记机关") @Excel(name = "登记机关")
private String registrationAuthority; private String registrationAuthority;
@@ -87,6 +151,12 @@ public class CreditNearbyCompanyImportParam implements Serializable {
@Excel(name = "是否小微企业") @Excel(name = "是否小微企业")
private String smallEnterprise; private String smallEnterprise;
@Excel(name = "企业(机构)类型")
private String institutionType;
@Excel(name = "企业规模")
private String companySize;
@Excel(name = "企业简介") @Excel(name = "企业简介")
private String companyProfile; private String companyProfile;

View File

@@ -63,6 +63,9 @@ public class CreditUserImportParam implements Serializable {
@Excel(name = "中标金额") @Excel(name = "中标金额")
private String winningPrice; private String winningPrice;
@Excel(name = "原告/上诉人")
private String plaintiffAppellant;
// @Excel(name = "备注") // @Excel(name = "备注")
// private String comments; // private String comments;
// //

View File

@@ -102,4 +102,7 @@ public class CreditUserParam extends BaseParam {
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer userId; private Integer userId;
@Schema(description = "申请人")
private String plaintiffAppellant;
} }

View File

@@ -1,6 +1,7 @@
package com.gxwebsoft.credit.param; package com.gxwebsoft.credit.param;
import cn.afterturn.easypoi.excel.annotation.Excel; import cn.afterturn.easypoi.excel.annotation.Excel;
import com.gxwebsoft.credit.excel.ExcelHeaderAlias;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -23,6 +24,7 @@ public class CreditXgxfImportParam implements Serializable {
private String dataType; private String dataType;
@Excel(name = "原告/上诉人") @Excel(name = "原告/上诉人")
@ExcelHeaderAlias({"申请人"})
private String plaintiffAppellant; private String plaintiffAppellant;
// Some upstream multi-company exports use "申请执行人" instead of "原告/上诉人". // Some upstream multi-company exports use "申请执行人" instead of "原告/上诉人".

View File

@@ -0,0 +1,24 @@
package com.gxwebsoft.credit.param;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 结束跟进流程DTO
*
* @author 科技小王子
* @since 2026-03-22
*/
@Data
@Schema(name = "EndFollowProcessDTO对象", description = "结束跟进流程DTO")
public class EndFollowProcessDTO {
@Schema(description = "客户ID", required = true)
@NotNull(message = "客户ID不能为空")
private Long customerId;
@Schema(description = "结束原因")
private String reason;
}

View File

@@ -0,0 +1,36 @@
package com.gxwebsoft.credit.param;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
/**
* 跟进步骤审核DTO
*
* @author 科技小王子
* @since 2026-03-22
*/
@Data
@Schema(name = "FollowStepApprovalDTO对象", description = "跟进步骤审核DTO")
public class FollowStepApprovalDTO {
@Schema(description = "客户ID", required = true)
@NotNull(message = "客户ID不能为空")
private Long customerId;
@Schema(description = "步骤号", required = true, example = "5")
@NotNull(message = "步骤号不能为空")
@Min(value = 1, message = "步骤号最小为1")
@Max(value = 7, message = "步骤号最大为7")
private Integer step;
@Schema(description = "是否审核通过", required = true)
@NotNull(message = "审核状态不能为空")
private Boolean approved;
@Schema(description = "审核备注")
private String remark;
}

View File

@@ -0,0 +1,29 @@
package com.gxwebsoft.credit.param;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
/**
* 跟进步骤查询DTO
*
* @author 科技小王子
* @since 2026-03-22
*/
@Data
@Schema(name = "FollowStepQueryDTO对象", description = "跟进步骤查询DTO")
public class FollowStepQueryDTO {
@Schema(description = "步骤号", example = "5")
@Min(value = 1, message = "步骤号最小为1")
@Max(value = 7, message = "步骤号最大为7")
private Integer step;
@Schema(description = "客户ID")
private Long customerId;
@Schema(description = "用户ID")
private Long userId;
}

View File

@@ -0,0 +1,85 @@
package com.gxwebsoft.credit.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
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 java.util.List;
/**
* 小程序端客户Service
*
* @author 科技小王子
* @since 2026-03-16 20:59:17
*/
public interface CreditMpCustomerService extends IService<CreditMpCustomer> {
/**
* 分页关联查询
*
* @param param 查询参数
* @return PageResult<CreditMpCustomer>
*/
PageResult<CreditMpCustomer> pageRel(CreditMpCustomerParam param);
/**
* 关联查询全部
*
* @param param 查询参数
* @return List<CreditMpCustomer>
*/
List<CreditMpCustomer> listRel(CreditMpCustomerParam param);
/**
* 根据id查询
*
* @param id ID
* @return CreditMpCustomer
*/
CreditMpCustomer getByIdRel(Integer id);
/**
* 审核跟进步骤
*
* @param dto 审核参数
*/
void approveFollowStep(FollowStepApprovalDTO dto);
/**
* 批量审核跟进步骤
*
* @param dto 批量审核参数
*/
void batchApproveFollowSteps(BatchFollowStepApprovalDTO dto);
/**
* 获取待审核的跟进步骤列表
*
* @param query 查询参数
* @return 待审核步骤列表
*/
List<PendingApprovalStepVO> getPendingApprovalSteps(FollowStepQueryDTO query);
/**
* 获取客户跟进统计
*
* @param customerId 客户ID
* @return 跟进统计信息
*/
FollowStatisticsDTO getFollowStatistics(Long customerId);
/**
* 结束客户跟进流程
*
* @param dto 结束流程参数
*/
void endFollowProcess(EndFollowProcessDTO dto);
}

View File

@@ -0,0 +1,259 @@
package com.gxwebsoft.credit.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.credit.mapper.CreditMpCustomerMapper;
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.FollowStepDetailDTO;
import com.gxwebsoft.credit.vo.PendingApprovalStepVO;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
/**
* 小程序端客户Service实现
*
* @author 科技小王子
* @since 2026-03-16 20:59:17
*/
@Service
public class CreditMpCustomerServiceImpl extends ServiceImpl<CreditMpCustomerMapper, CreditMpCustomer> implements CreditMpCustomerService {
private final BaseController baseController = new BaseController();
@Override
public PageResult<CreditMpCustomer> pageRel(CreditMpCustomerParam param) {
PageParam<CreditMpCustomer, CreditMpCustomerParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<CreditMpCustomer> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<CreditMpCustomer> listRel(CreditMpCustomerParam param) {
List<CreditMpCustomer> list = baseMapper.selectListRel(param);
// 排序
PageParam<CreditMpCustomer, CreditMpCustomerParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time desc");
return page.sortRecords(list);
}
@Override
public CreditMpCustomer getByIdRel(Integer id) {
CreditMpCustomerParam param = new CreditMpCustomerParam();
param.setId(id);
return param.getOne(baseMapper.selectListRel(param));
}
@Override
@Transactional
public void approveFollowStep(FollowStepApprovalDTO dto) {
CreditMpCustomer customer = getById(dto.getCustomerId());
if (customer == null) {
throw new BusinessException("客户不存在");
}
// 验证步骤是否已提交
if (!isStepSubmitted(customer, dto.getStep())) {
throw new BusinessException("该步骤尚未提交,无法审核");
}
// 更新审核状态
updateStepApproval(customer, 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 BusinessException("客户不存在");
}
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(EndFollowProcessDTO dto) {
CreditMpCustomer customer = getById(dto.getCustomerId());
if (customer == null) {
throw new BusinessException("客户不存在");
}
customer.setFollowProcessEnded(1);
customer.setFollowProcessEndTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
customer.setFollowProcessEndReason(dto.getReason());
updateById(customer);
}
// 私有辅助方法
private boolean isStepSubmitted(CreditMpCustomer customer, Integer step) {
switch (step) {
case 1: return customer.getFollowStep1Submitted() != null && customer.getFollowStep1Submitted() == 1;
case 2: return customer.getFollowStep2Submitted() != null && customer.getFollowStep2Submitted() == 1;
case 3: return customer.getFollowStep3Submitted() != null && customer.getFollowStep3Submitted() == 1;
case 4: return customer.getFollowStep4Submitted() != null && customer.getFollowStep4Submitted() == 1;
case 5: return customer.getFollowStep5Submitted() != null && customer.getFollowStep5Submitted() == 1;
case 6: return customer.getFollowStep6Submitted() != null && customer.getFollowStep6Submitted() == 1;
case 7: return customer.getFollowStep7Submitted() != null && customer.getFollowStep7Submitted() == 1;
default: return false;
}
}
private void updateStepApproval(CreditMpCustomer customer, FollowStepApprovalDTO dto) {
String currentTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Integer currentUserId = baseController.getLoginUserId();
switch (dto.getStep()) {
case 5:
customer.setFollowStep5Approved(dto.getApproved() ? 1 : 0);
customer.setFollowStep5ApprovedAt(currentTime);
customer.setFollowStep5ApprovedBy(currentUserId != null ? currentUserId.longValue() : null);
break;
case 6:
customer.setFollowStep6Approved(dto.getApproved() ? 1 : 0);
customer.setFollowStep6ApprovedAt(currentTime);
customer.setFollowStep6ApprovedBy(currentUserId != null ? currentUserId.longValue() : null);
break;
case 7:
customer.setFollowStep7Approved(dto.getApproved() ? 1 : 0);
customer.setFollowStep7ApprovedAt(currentTime);
customer.setFollowStep7ApprovedBy(currentUserId != null ? currentUserId.longValue() : null);
break;
}
updateById(customer);
}
private void updateCustomerStep(CreditMpCustomer customer, Integer step) {
// 更新客户的总体步骤状态
if (step >= customer.getStep()) {
customer.setStep(step + 1);
updateById(customer);
}
}
private FollowStepDetailDTO getStepDetail(CreditMpCustomer customer, Integer step) {
FollowStepDetailDTO detail = new FollowStepDetailDTO();
detail.setStep(step);
// 设置步骤标题
String[] stepTitles = {"", "案件受理", "材料准备", "案件办理", "送达签收", "合同签订", "订单回款", "电话回访"};
detail.setTitle(stepTitles[step]);
// 获取步骤状态
boolean submitted = isStepSubmitted(customer, step);
boolean approved = isStepApproved(customer, step);
if (approved) {
detail.setStatus("approved");
detail.setApprovedAt(getStepApprovedAt(customer, step));
} else if (submitted) {
detail.setStatus("submitted");
detail.setSubmittedAt(getStepSubmittedAt(customer, step));
} else {
detail.setStatus("pending");
}
return detail;
}
private boolean isStepApproved(CreditMpCustomer customer, Integer step) {
switch (step) {
case 1: return customer.getFollowStep1Approved() != null && customer.getFollowStep1Approved() == 1;
case 2: return customer.getFollowStep2Approved() != null && customer.getFollowStep2Approved() == 1;
case 3: return customer.getFollowStep3Approved() != null && customer.getFollowStep3Approved() == 1;
case 4: return customer.getFollowStep4Approved() != null && customer.getFollowStep4Approved() == 1;
case 5: return customer.getFollowStep5Approved() != null && customer.getFollowStep5Approved() == 1;
case 6: return customer.getFollowStep6Approved() != null && customer.getFollowStep6Approved() == 1;
case 7: return customer.getFollowStep7Approved() != null && customer.getFollowStep7Approved() == 1;
default: return false;
}
}
private LocalDateTime getStepSubmittedAt(CreditMpCustomer customer, Integer step) {
String submittedAt = null;
switch (step) {
case 1: submittedAt = customer.getFollowStep1SubmittedAt(); break;
case 2: submittedAt = customer.getFollowStep2SubmittedAt(); break;
case 3: submittedAt = customer.getFollowStep3SubmittedAt(); break;
case 4: submittedAt = customer.getFollowStep4SubmittedAt(); break;
case 5: submittedAt = customer.getFollowStep5SubmittedAt(); break;
case 6: submittedAt = customer.getFollowStep6SubmittedAt(); break;
case 7: submittedAt = customer.getFollowStep7SubmittedAt(); break;
}
return submittedAt != null ? LocalDateTime.parse(submittedAt, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null;
}
private LocalDateTime getStepApprovedAt(CreditMpCustomer customer, Integer step) {
String approvedAt = null;
switch (step) {
case 1: approvedAt = customer.getFollowStep1ApprovedAt(); break;
case 2: approvedAt = customer.getFollowStep2ApprovedAt(); break;
case 3: approvedAt = customer.getFollowStep3ApprovedAt(); break;
case 4: approvedAt = customer.getFollowStep4ApprovedAt(); break;
case 5: approvedAt = customer.getFollowStep5ApprovedAt(); break;
case 6: approvedAt = customer.getFollowStep6ApprovedAt(); break;
case 7: approvedAt = customer.getFollowStep7ApprovedAt(); break;
}
return approvedAt != null ? LocalDateTime.parse(approvedAt, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null;
}
}

View File

@@ -0,0 +1,32 @@
package com.gxwebsoft.credit.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 客户跟进统计DTO
*
* @author 科技小王子
* @since 2026-03-22
*/
@Data
@Schema(name = "FollowStatisticsDTO对象", description = "客户跟进统计DTO")
public class FollowStatisticsDTO {
@Schema(description = "总步骤数")
private Integer totalSteps;
@Schema(description = "已完成步骤数")
private Integer completedSteps;
@Schema(description = "当前步骤")
private Integer currentStep;
@Schema(description = "进度百分比")
private Double progress;
@Schema(description = "步骤详情列表")
private List<FollowStepDetailDTO> stepDetails;
}

View File

@@ -0,0 +1,32 @@
package com.gxwebsoft.credit.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 跟进步骤详情DTO
*
* @author 科技小王子
* @since 2026-03-22
*/
@Data
@Schema(name = "FollowStepDetailDTO对象", description = "跟进步骤详情DTO")
public class FollowStepDetailDTO {
@Schema(description = "步骤号")
private Integer step;
@Schema(description = "步骤标题")
private String title;
@Schema(description = "状态: pending, submitted, approved, rejected")
private String status;
@Schema(description = "提交时间")
private LocalDateTime submittedAt;
@Schema(description = "审核时间")
private LocalDateTime approvedAt;
}

View File

@@ -0,0 +1,38 @@
package com.gxwebsoft.credit.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 待审核跟进步骤VO
*
* @author 科技小王子
* @since 2026-03-22
*/
@Data
@Schema(name = "PendingApprovalStepVO对象", description = "待审核跟进步骤VO")
public class PendingApprovalStepVO {
@Schema(description = "客户ID")
private Long customerId;
@Schema(description = "客户名称")
private String customerName;
@Schema(description = "步骤号")
private Integer step;
@Schema(description = "步骤标题")
private String stepTitle;
@Schema(description = "提交时间")
private LocalDateTime submittedAt;
@Schema(description = "提交人")
private String submittedBy;
@Schema(description = "内容")
private String content;
}

View File

@@ -52,6 +52,34 @@ public class GltTicketOrderController extends BaseController {
return success(gltTicketOrderService.pageRel(param)); return success(gltTicketOrderService.pageRel(param));
} }
@PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员端:分页查询可接单的送水订单")
@GetMapping("/rider/available")
public ApiResult<PageResult<GltTicketOrder>> riderAvailablePage(GltTicketOrderParam param) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录", null);
}
Integer tenantId = getTenantId();
param.setTenantId(tenantId);
// 仅允许配送员访问
requireActiveRider(loginUser.getUserId(), tenantId);
// 查询未分配的待配送订单riderId 为空或0
// 设置为0表示查询未分配的订单XML中会处理为 IS NULL OR = 0
param.setRiderId(0);
if (param.getDeliveryStatus() == null) {
param.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING);
}
// 配送员端默认按期望配送时间优先
if (StrUtil.isBlank(param.getSort())) {
param.setSort("sendTime asc, createTime desc");
}
// 使用现有的关联查询方法,通过参数控制
return success(gltTicketOrderService.pageRel(param));
}
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员端:分页查询我的送水订单") @Operation(summary = "配送员端:分页查询我的送水订单")
@GetMapping("/rider/page") @GetMapping("/rider/page")
@@ -65,8 +93,12 @@ public class GltTicketOrderController extends BaseController {
// 仅允许配送员访问 // 仅允许配送员访问
requireActiveRider(loginUser.getUserId(), tenantId); requireActiveRider(loginUser.getUserId(), tenantId);
// 关键修复:配送员只能看到分配给自己的订单
param.setRiderId(loginUser.getUserId()); param.setRiderId(loginUser.getUserId());
// 默认查询待配送和配送中的订单
if (param.getDeliveryStatus() == null) { if (param.getDeliveryStatus() == null) {
// 可以通过参数传递多个状态,这里简化为只查待配送
param.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING); param.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING);
} }
// 配送员端默认按期望配送时间优先 // 配送员端默认按期望配送时间优先
@@ -241,8 +273,33 @@ public class GltTicketOrderController extends BaseController {
@Operation(summary = "修改送水订单") @Operation(summary = "修改送水订单")
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) { public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) {
if (gltTicketOrderService.updateById(gltTicketOrder)) { if (gltTicketOrder == null || gltTicketOrder.getId() == null) {
return fail("订单ID不能为空");
}
Integer tenantId = getTenantId(); Integer tenantId = getTenantId();
// 根据 addressId 同步更新订单地址快照
if (gltTicketOrder.getAddressId() != null) {
ShopUserAddress userAddress = shopUserAddressService.getByIdRel(gltTicketOrder.getAddressId());
if (userAddress == null) {
return fail("收货地址不存在");
}
if (tenantId != null && userAddress.getTenantId() != null && !tenantId.equals(userAddress.getTenantId())) {
return fail("收货地址不存在或无权限");
}
GltTicketOrder oldOrder = gltTicketOrderService.getById(gltTicketOrder.getId());
if (oldOrder == null) {
return fail("订单不存在");
}
Integer targetUserId = gltTicketOrder.getUserId() != null ? gltTicketOrder.getUserId() : oldOrder.getUserId();
if (targetUserId != null && userAddress.getUserId() != null && !targetUserId.equals(userAddress.getUserId())) {
return fail("收货地址不存在或无权限");
}
gltTicketOrder.setAddressId(userAddress.getId());
gltTicketOrder.setAddress(buildAddressSnapshot(userAddress));
}
if (gltTicketOrderService.updateById(gltTicketOrder)) {
// 后台指派配送员(直接改 riderId同步商城订单为“已发货”(deliveryStatus=20) // 后台指派配送员(直接改 riderId同步商城订单为“已发货”(deliveryStatus=20)
if (gltTicketOrder != null if (gltTicketOrder != null
&& gltTicketOrder.getId() != null && gltTicketOrder.getId() != null

View File

@@ -47,7 +47,6 @@ public class GltTicketTemplateController extends BaseController {
return success(gltTicketTemplateService.listRel(param)); return success(gltTicketTemplateService.listRel(param));
} }
@PreAuthorize("hasAuthority('glt:gltTicketTemplate:list')")
@Operation(summary = "根据id查询水票") @Operation(summary = "根据id查询水票")
@GetMapping("/{id}") @GetMapping("/{id}")
public ApiResult<GltTicketTemplate> get(@PathVariable("id") Integer id) { public ApiResult<GltTicketTemplate> get(@PathVariable("id") Integer id) {

View File

@@ -35,6 +35,10 @@ public class GltTicketOrder implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String orderNo; private String orderNo;
@Schema(description = "订单状态")
@TableField(exist = false)
private Integer orderStatus;
@Schema(description = "门店ID") @Schema(description = "门店ID")
private Integer storeId; private Integer storeId;

View File

@@ -58,6 +58,9 @@ public class GltTicketTemplate implements Serializable {
@Schema(description = "首期释放时机0=支付成功当刻1=下个月同日") @Schema(description = "首期释放时机0=支付成功当刻1=下个月同日")
private Integer firstReleaseMode; private Integer firstReleaseMode;
@Schema(description = "步长")
private Integer step;
@Schema(description = "用户ID") @Schema(description = "用户ID")
private Integer userId; private Integer userId;

View File

@@ -88,6 +88,10 @@ public class GltUserTicket implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String phone; private String phone;
@Schema(description = "订单状态")
@TableField(exist = false)
private Integer orderStatus;
@Schema(description = "排序(数字越小越靠前)") @Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber; private Integer sortNumber;

View File

@@ -11,7 +11,7 @@
d.name as receiverName, d.phone as receiverPhone, d.name as receiverName, d.phone as receiverPhone,
d.province as receiverProvince, d.city as receiverCity, d.region as receiverRegion, d.province as receiverProvince, d.city as receiverCity, d.region as receiverRegion,
d.address as receiverAddress, d.full_address as receiverFullAddress, d.lat as receiverLat, d.lng as receiverLng, d.address as receiverAddress, d.full_address as receiverFullAddress, d.lat as receiverLat, d.lng as receiverLng,
COALESCE(o.order_no, f.order_no) as orderNo COALESCE(o.order_no, f.order_no) as orderNo, o.order_status as orderStatus
FROM glt_ticket_order a FROM glt_ticket_order a
LEFT JOIN shop_store b ON a.store_id = b.id LEFT JOIN shop_store b ON a.store_id = b.id
LEFT JOIN shop_store_warehouse w ON a.warehouse_id = w.id LEFT JOIN shop_store_warehouse w ON a.warehouse_id = w.id
@@ -32,8 +32,13 @@
AND a.store_id = #{param.storeId} AND a.store_id = #{param.storeId}
</if> </if>
<if test="param.riderId != null"> <if test="param.riderId != null">
<if test="param.riderId == 0">
AND (a.rider_id IS NULL OR a.rider_id = 0)
</if>
<if test="param.riderId != 0">
AND a.rider_id = #{param.riderId} AND a.rider_id = #{param.riderId}
</if> </if>
</if>
<if test="param.deliveryStatus != null"> <if test="param.deliveryStatus != null">
AND a.delivery_status = #{param.deliveryStatus} AND a.delivery_status = #{param.deliveryStatus}
</if> </if>

View File

@@ -4,12 +4,12 @@
<!-- 关联查询sql --> <!-- 关联查询sql -->
<sql id="selectSql"> <sql id="selectSql">
SELECT a.*, u.nickname, u.avatar, u.phone, m.name AS templateName, o.pay_price AS payPrice SELECT a.*, u.nickname, u.avatar, u.phone, m.name AS templateName, o.pay_price AS payPrice, o.order_status as orderStatus
FROM glt_user_ticket a FROM glt_user_ticket a
LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id
LEFT JOIN glt_ticket_template m ON a.template_id = m.id LEFT JOIN glt_ticket_template m ON a.template_id = m.id
<!-- 使用 order_id + tenant_id 关联,避免 order_no 跨租户/重复导致 a.id 数据被 JOIN 放大 --> <!-- 使用 order_id + tenant_id 关联,避免 order_no 跨租户/重复导致 a.id 数据被 JOIN 放大 -->
LEFT JOIN shop_order o ON a.order_id = o.order_id AND a.tenant_id = o.tenant_id AND o.deleted = 0 LEFT JOIN shop_order o ON a.order_no = o.order_no AND a.tenant_id = o.tenant_id AND o.deleted = 0
<where> <where>
<if test="param.id != null"> <if test="param.id != null">
AND a.id = #{param.id} AND a.id = #{param.id}
@@ -26,6 +26,9 @@
<if test="param.orderNo != null"> <if test="param.orderNo != null">
AND a.order_no LIKE CONCAT('%', #{param.orderNo}, '%') AND a.order_no LIKE CONCAT('%', #{param.orderNo}, '%')
</if> </if>
<if test="param.orderStatus != null">
AND o.order_status = #{param.orderStatus}
</if>
<if test="param.orderGoodsId != null"> <if test="param.orderGoodsId != null">
AND a.order_goods_id = #{param.orderGoodsId} AND a.order_goods_id = #{param.orderGoodsId}
</if> </if>

View File

@@ -1,5 +1,6 @@
package com.gxwebsoft.glt.param; package com.gxwebsoft.glt.param;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.gxwebsoft.common.core.annotation.QueryField; import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType; import com.gxwebsoft.common.core.annotation.QueryType;
@@ -38,6 +39,9 @@ public class GltTicketOrderParam extends BaseParam {
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer riderId; private Integer riderId;
@Schema(description = "订单编号")
private String orderNo;
@Schema(description = "配送状态10待配送、20配送中、30待客户确认、40已完成") @Schema(description = "配送状态10待配送、20配送中、30待客户确认、40已完成")
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer deliveryStatus; private Integer deliveryStatus;
@@ -83,4 +87,8 @@ public class GltTicketOrderParam extends BaseParam {
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer deleted; private Integer deleted;
@Schema(description = "订单状态")
@QueryField(type = QueryType.EQ)
private Integer orderStatus;
} }

View File

@@ -40,6 +40,10 @@ public class GltUserTicketParam extends BaseParam {
@Schema(description = "订单编号") @Schema(description = "订单编号")
private String orderNo; private String orderNo;
@Schema(description = "订单状态")
@QueryField(type = QueryType.EQ)
private Integer orderStatus;
@Schema(description = "订单商品ID") @Schema(description = "订单商品ID")
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer orderGoodsId; private Integer orderGoodsId;

View File

@@ -2,27 +2,22 @@ package com.gxwebsoft.glt.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.entity.GltTicketTemplate; import com.gxwebsoft.glt.entity.GltTicketTemplate;
import com.gxwebsoft.glt.entity.GltUserTicket; import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.entity.GltUserTicketLog; import com.gxwebsoft.glt.entity.GltUserTicketLog;
import com.gxwebsoft.glt.entity.GltUserTicketRelease; import com.gxwebsoft.glt.entity.GltUserTicketRelease;
import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.entity.ShopOrderGoods; import com.gxwebsoft.shop.entity.ShopOrderGoods;
import com.gxwebsoft.shop.entity.ShopUserAddress;
import com.gxwebsoft.shop.service.ShopOrderGoodsService; import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService; import com.gxwebsoft.shop.service.ShopOrderService;
import com.gxwebsoft.shop.service.ShopUserAddressService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -43,8 +38,6 @@ import java.util.Set;
public class GltTicketIssueService { public class GltTicketIssueService {
public static final int CHANGE_TYPE_ISSUE = 10; public static final int CHANGE_TYPE_ISSUE = 10;
/** 变更类型:起始送水自动核销(按模板 startSendQty 在发放时自动消耗) */
public static final int CHANGE_TYPE_START_SEND_WRITE_OFF = 12;
private enum IssueOutcome { private enum IssueOutcome {
ISSUED, ISSUED,
@@ -60,8 +53,6 @@ public class GltTicketIssueService {
private final GltUserTicketService gltUserTicketService; private final GltUserTicketService gltUserTicketService;
private final GltUserTicketReleaseService gltUserTicketReleaseService; private final GltUserTicketReleaseService gltUserTicketReleaseService;
private final GltUserTicketLogService gltUserTicketLogService; private final GltUserTicketLogService gltUserTicketLogService;
private final GltTicketOrderService gltTicketOrderService;
private final ShopUserAddressService shopUserAddressService;
private final TransactionTemplate transactionTemplate; private final TransactionTemplate transactionTemplate;
/** /**
@@ -258,7 +249,7 @@ public class GltTicketIssueService {
int giftMultiplier = template.getGiftMultiplier() != null ? template.getGiftMultiplier() : 0; int giftMultiplier = template.getGiftMultiplier() != null ? template.getGiftMultiplier() : 0;
int giftQty = buyQty * Math.max(giftMultiplier, 0); int giftQty = buyQty * Math.max(giftMultiplier, 0);
// 购买量buyQty应立即可用赠送量giftQty进入冻结并按计划释放。 // 总票数(购买量 + 赠送量)
int totalQty = buyQty + giftQty; int totalQty = buyQty + giftQty;
if (totalQty <= 0) { if (totalQty <= 0) {
@@ -269,6 +260,15 @@ public class GltTicketIssueService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
boolean useReleasePeriods = template.getReleasePeriods() != null && template.getReleasePeriods() > 0;
// 释放期数releasePeriods为高优先级
// - 配置了期数:按期数平均分摊 totalQty每期释放不再“先把购买桶数一次性释放”。
// - 未配置期数:保持原逻辑(购买量立即可用,赠送量冻结并按计划释放)。
int initAvailableQty = useReleasePeriods ? 0 : buyQty;
int initFrozenQty = useReleasePeriods ? totalQty : giftQty;
int initReleasedQty = useReleasePeriods ? 0 : buyQty;
GltUserTicket userTicket = new GltUserTicket(); GltUserTicket userTicket = new GltUserTicket();
userTicket.setTemplateId(template.getId()); userTicket.setTemplateId(template.getId());
userTicket.setGoodsId(og.getGoodsId()); userTicket.setGoodsId(og.getGoodsId());
@@ -276,11 +276,10 @@ public class GltTicketIssueService {
userTicket.setOrderNo(order.getOrderNo()); userTicket.setOrderNo(order.getOrderNo());
userTicket.setOrderGoodsId(og.getId()); userTicket.setOrderGoodsId(og.getId());
userTicket.setTotalQty(totalQty); userTicket.setTotalQty(totalQty);
userTicket.setAvailableQty(buyQty); userTicket.setAvailableQty(initAvailableQty);
userTicket.setFrozenQty(giftQty); userTicket.setFrozenQty(initFrozenQty);
userTicket.setUsedQty(0); userTicket.setUsedQty(0);
// 初始可用量来自“购买量”,视为已释放 userTicket.setReleasedQty(initReleasedQty);
userTicket.setReleasedQty(buyQty);
userTicket.setOrderGoodsQty(og.getTotalNum()); userTicket.setOrderGoodsQty(og.getTotalNum());
userTicket.setUserId(order.getUserId()); userTicket.setUserId(order.getUserId());
userTicket.setSortNumber(0); userTicket.setSortNumber(0);
@@ -293,12 +292,36 @@ public class GltTicketIssueService {
gltUserTicketService.save(userTicket); gltUserTicketService.save(userTicket);
// 生成释放计划(按月) // 生成释放计划
// - 配置 releasePeriods按 totalQty 生成每期释放量periods 优先)
// - 未配置 releasePeriods按 giftQty 生成每期释放量
LocalDateTime baseTime = order.getPayTime() != null ? order.getPayTime() : order.getCreateTime(); LocalDateTime baseTime = order.getPayTime() != null ? order.getPayTime() : order.getCreateTime();
if (baseTime == null) { if (baseTime == null) {
baseTime = now; baseTime = now;
} }
List<GltUserTicketRelease> releases = buildReleasePlan(template, userTicket, baseTime, giftQty, now); int planQty = useReleasePeriods ? totalQty : giftQty;
List<GltUserTicketRelease> releases = buildReleasePlan(template, userTicket, baseTime, planQty, now);
// 若启用了 releasePeriods 且首期释放时机为“支付成功当刻”,则将首期释放量直接计入可用,
// 避免用户刚购买后短时间内无可用水票;后续期数仍由自动释放任务按 release_time 释放。
if (useReleasePeriods && !releases.isEmpty() && !Objects.equals(template.getFirstReleaseMode(), 1)) {
GltUserTicketRelease first = releases.get(0);
Integer firstQtyObj = first.getReleaseQty();
LocalDateTime firstTime = first.getReleaseTime();
int firstQty = firstQtyObj != null ? firstQtyObj : 0;
if (firstQty > 0 && (firstTime == null || !firstTime.isAfter(now))) {
first.setStatus(1);
first.setUpdateTime(now);
userTicket.setAvailableQty((userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0) + firstQty);
userTicket.setFrozenQty((userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0) - firstQty);
userTicket.setReleasedQty((userTicket.getReleasedQty() != null ? userTicket.getReleasedQty() : 0) + firstQty);
userTicket.setUpdateTime(now);
if (!gltUserTicketService.updateById(userTicket)) {
throw new IllegalStateException("首期释放:更新用户水票失败 userTicketId=" + userTicket.getId());
}
}
}
if (!releases.isEmpty()) { if (!releases.isEmpty()) {
gltUserTicketReleaseService.saveBatch(releases); gltUserTicketReleaseService.saveBatch(releases);
} }
@@ -307,11 +330,11 @@ public class GltTicketIssueService {
GltUserTicketLog issueLog = new GltUserTicketLog(); GltUserTicketLog issueLog = new GltUserTicketLog();
issueLog.setUserTicketId(userTicket.getId()); issueLog.setUserTicketId(userTicket.getId());
issueLog.setChangeType(CHANGE_TYPE_ISSUE); issueLog.setChangeType(CHANGE_TYPE_ISSUE);
issueLog.setChangeAvailable(buyQty); issueLog.setChangeAvailable(userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0);
issueLog.setChangeFrozen(giftQty); issueLog.setChangeFrozen(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0);
issueLog.setChangeUsed(0); issueLog.setChangeUsed(0);
issueLog.setAvailableAfter(buyQty); issueLog.setAvailableAfter(userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0);
issueLog.setFrozenAfter(giftQty); issueLog.setFrozenAfter(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0);
issueLog.setUsedAfter(0); issueLog.setUsedAfter(0);
issueLog.setOrderId(order.getOrderId()); issueLog.setOrderId(order.getOrderId());
issueLog.setOrderNo(order.getOrderNo()); issueLog.setOrderNo(order.getOrderNo());
@@ -325,51 +348,13 @@ public class GltTicketIssueService {
issueLog.setUpdateTime(now); issueLog.setUpdateTime(now);
gltUserTicketLogService.save(issueLog); gltUserTicketLogService.save(issueLog);
// 按模板配置:自动“使用掉第一次水票”(起始送水数量) // 按整改需求:水票购买(囤券预付费)与水票核销(下单履约)应为两次独立用户动作;
// 因此模板 startSendQty 不再在“发放”阶段自动核销/自动生成送水订单。
Integer startSendQtyObj = template.getStartSendQty(); Integer startSendQtyObj = template.getStartSendQty();
int startSendQty = startSendQtyObj != null ? startSendQtyObj : 0; int startSendQty = startSendQtyObj != null ? startSendQtyObj : 0;
if (startSendQty > 0) { if (startSendQty > 0) {
int availableBefore = userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0; log.info("套票模板配置了 startSendQty但不再自动送水/自动核销 - tenantId={}, orderNo={}, templateId={}, userTicketId={}, startSendQty={}",
int usedBefore = userTicket.getUsedQty() != null ? userTicket.getUsedQty() : 0; tenantId, order.getOrderNo(), template.getId(), userTicket.getId(), startSendQty);
int toUse = Math.min(startSendQty, availableBefore);
if (toUse > 0) {
log.info("起始送水自动核销 - tenantId={}, orderNo={}, templateId={}, userTicketId={}, startSendQty={}, availableBefore={}, toUse={}",
tenantId, order.getOrderNo(), template.getId(), userTicket.getId(), startSendQty, availableBefore, toUse);
userTicket.setAvailableQty(availableBefore - toUse);
userTicket.setUsedQty(usedBefore + toUse);
userTicket.setUpdateTime(now);
if (!gltUserTicketService.updateById(userTicket)) {
throw new IllegalStateException("起始送水自动核销:更新用户水票失败 userTicketId=" + userTicket.getId());
}
// 起始送水:自动核销成功后,生成一条送水订单(用于配送端/后台跟踪)
GltTicketOrder ticketOrder = buildStartSendTicketOrder(tenantId, order, userTicket, toUse, now);
if (!gltTicketOrderService.save(ticketOrder)) {
throw new IllegalStateException("起始送水自动核销:创建送水订单失败 userTicketId=" + userTicket.getId());
}
GltUserTicketLog writeOffLog = new GltUserTicketLog();
writeOffLog.setUserTicketId(userTicket.getId());
writeOffLog.setChangeType(CHANGE_TYPE_START_SEND_WRITE_OFF);
writeOffLog.setChangeAvailable(-toUse);
writeOffLog.setChangeFrozen(0);
writeOffLog.setChangeUsed(toUse);
writeOffLog.setAvailableAfter(userTicket.getAvailableQty());
writeOffLog.setFrozenAfter(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0);
writeOffLog.setUsedAfter(userTicket.getUsedQty());
// 关联送水订单(保持与“用户下单核销”的日志一致),并在备注里保留来源商城订单号便于追溯
writeOffLog.setOrderId(ticketOrder.getId());
writeOffLog.setOrderNo(ticketOrder.getId() == null ? null : String.valueOf(ticketOrder.getId()));
writeOffLog.setUserId(order.getUserId());
writeOffLog.setSortNumber(0);
writeOffLog.setComments("起始送水自动核销(来源商城订单:" + safe(order.getOrderNo()) + ")");
writeOffLog.setStatus(0);
writeOffLog.setDeleted(0);
writeOffLog.setTenantId(tenantId);
writeOffLog.setCreateTime(now);
writeOffLog.setUpdateTime(now);
gltUserTicketLogService.save(writeOffLog);
}
} }
log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, orderGoodsQty={}, buyQty={}, giftQty={}, startSendQty={}, totalQty={}", log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, orderGoodsQty={}, buyQty={}, giftQty={}, startSendQty={}, totalQty={}",
@@ -402,12 +387,13 @@ public class GltTicketIssueService {
if (releasePeriods != null && releasePeriods > 0) { if (releasePeriods != null && releasePeriods > 0) {
int base = totalQty / releasePeriods; int base = totalQty / releasePeriods;
int remainder = totalQty % releasePeriods; int remainder = totalQty % releasePeriods;
for (int i = 1; i <= releasePeriods; i++) { // periodNo 从 0 开始第0期、第1期……更贴近任务执行计数
int qty = base + (i <= remainder ? 1 : 0); for (int i = 0; i < releasePeriods; i++) {
int qty = base + (i < remainder ? 1 : 0);
if (qty <= 0) { if (qty <= 0) {
continue; continue;
} }
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i - 1), now)); list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i), now));
} }
return list; return list;
} }
@@ -417,13 +403,14 @@ public class GltTicketIssueService {
: 10; : 10;
int periods = (totalQty + monthlyReleaseQty - 1) / monthlyReleaseQty; int periods = (totalQty + monthlyReleaseQty - 1) / monthlyReleaseQty;
int remaining = totalQty; int remaining = totalQty;
for (int i = 1; i <= periods; i++) { // periodNo 从 0 开始第0期、第1期……
for (int i = 0; i < periods; i++) {
int qty = Math.min(monthlyReleaseQty, remaining); int qty = Math.min(monthlyReleaseQty, remaining);
if (qty <= 0) { if (qty <= 0) {
break; break;
} }
remaining -= qty; remaining -= qty;
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i - 1), now)); list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i), now));
} }
return list; return list;
@@ -458,77 +445,4 @@ public class GltTicketIssueService {
return LocalDateTime.of(adjusted, time); return LocalDateTime.of(adjusted, time);
} }
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private GltTicketOrder buildStartSendTicketOrder(Integer tenantId,
ShopOrder shopOrder,
GltUserTicket userTicket,
int totalNum,
LocalDateTime now) {
GltTicketOrder o = new GltTicketOrder();
o.setUserTicketId(userTicket.getId());
o.setUserId(shopOrder.getUserId());
o.setStoreId(shopOrder.getStoreId());
o.setWarehouseId(shopOrder.getWarehouseId());
o.setRiderId(shopOrder.getRiderId());
o.setTotalNum(totalNum);
o.setPrice(BigDecimal.ZERO);
o.setBuyerRemarks(shopOrder.getBuyerRemarks());
// 地址快照:优先使用地址表快照;兜底使用商城订单上的 address 字段
Integer addressId = shopOrder.getAddressId();
o.setAddressId(addressId);
String addressSnapshot = null;
if (addressId != null) {
ShopUserAddress addr = shopUserAddressService.getOne(new LambdaQueryWrapper<ShopUserAddress>()
.eq(ShopUserAddress::getId, addressId)
.eq(ShopUserAddress::getUserId, shopOrder.getUserId())
.eq(ShopUserAddress::getTenantId, tenantId)
.last("limit 1"));
addressSnapshot = buildAddressSnapshot(addr);
}
if (addressSnapshot == null || addressSnapshot.isBlank()) {
addressSnapshot = shopOrder.getAddress();
}
o.setAddress(addressSnapshot);
String preferredSendTime = shopOrder.getSendStartTime();
if (preferredSendTime == null || preferredSendTime.isBlank()) {
preferredSendTime = now.format(DATETIME_FMT);
}
o.setSendTime(preferredSendTime);
o.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING);
o.setSortNumber(0);
o.setComments("起始送水自动下单(来源商城订单:" + safe(shopOrder.getOrderNo()) + ")");
o.setStatus(0);
o.setDeleted(0);
o.setTenantId(tenantId);
o.setCreateTime(now);
o.setUpdateTime(now);
return o;
}
private static String buildAddressSnapshot(ShopUserAddress addr) {
if (addr == null) {
return null;
}
if (addr.getFullAddress() != null && !addr.getFullAddress().isBlank()) {
return addr.getFullAddress();
}
// 兼容旧数据fullAddress 为空时,拼接省市区 + 详细地址
StringBuilder sb = new StringBuilder();
if (addr.getProvince() != null) sb.append(addr.getProvince());
if (addr.getCity() != null) sb.append(addr.getCity());
if (addr.getRegion() != null) sb.append(addr.getRegion());
if (addr.getAddress() != null) sb.append(addr.getAddress());
String s = sb.toString();
if (!s.isBlank()) {
return s;
}
return addr.getAddress();
}
private static String safe(String s) {
return s == null ? "" : s;
}
} }

View File

@@ -0,0 +1,167 @@
package com.gxwebsoft.glt.service;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.entity.GltUserTicketRelease;
import com.gxwebsoft.shop.entity.ShopOrderGoods;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 水票撤销(订单取消/退款成功后的清理):
* - 取消/隐藏用户水票glt_user_ticket.deleted=1
* - 删除未完成的释放计划glt_user_ticket_release.deleted=1, status!=1
* - 删除未完成的送水订单glt_ticket_order.deleted=1, delivery_status!=40
*
* <p>说明:该操作需保证幂等;若无关联水票则无任何副作用。</p>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class GltTicketRevokeService {
/** release.status已释放 */
private static final int RELEASE_STATUS_DONE = 1;
/** ticketOrder.deliveryStatus已完成 */
private static final int TICKET_ORDER_DELIVERY_STATUS_FINISHED = 40;
private final GltUserTicketService gltUserTicketService;
private final GltUserTicketReleaseService gltUserTicketReleaseService;
private final GltTicketOrderService gltTicketOrderService;
private final ShopOrderGoodsService shopOrderGoodsService;
@Transactional(rollbackFor = Exception.class)
public int revokeByShopOrder(Integer tenantId, Integer shopOrderId, String shopOrderNo, String reason) {
if (tenantId == null) {
return 0;
}
if (shopOrderId == null && StrUtil.isBlank(shopOrderNo)) {
return 0;
}
LambdaQueryWrapper<GltUserTicket> qw = new LambdaQueryWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0);
if (shopOrderId != null && StrUtil.isNotBlank(shopOrderNo)) {
qw.and(w -> w.eq(GltUserTicket::getOrderId, shopOrderId).or().eq(GltUserTicket::getOrderNo, shopOrderNo));
} else if (shopOrderId != null) {
qw.eq(GltUserTicket::getOrderId, shopOrderId);
} else {
qw.eq(GltUserTicket::getOrderNo, shopOrderNo);
}
List<GltUserTicket> tickets = gltUserTicketService.list(qw);
// 兼容历史数据:部分水票只记录了 orderGoodsId未记录 orderId/orderNo
if ((tickets == null || tickets.isEmpty()) && shopOrderId != null) {
try {
List<ShopOrderGoods> goodsList = shopOrderGoodsService.list(
new LambdaQueryWrapper<ShopOrderGoods>()
.select(ShopOrderGoods::getId)
.eq(ShopOrderGoods::getTenantId, tenantId)
.eq(ShopOrderGoods::getOrderId, shopOrderId)
);
List<Integer> orderGoodsIds = goodsList == null ? List.of() : goodsList.stream()
.map(ShopOrderGoods::getId)
.filter(id -> id != null && id > 0)
.distinct()
.toList();
if (!orderGoodsIds.isEmpty()) {
tickets = gltUserTicketService.list(
new LambdaQueryWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.in(GltUserTicket::getOrderGoodsId, orderGoodsIds)
);
}
} catch (Exception e) {
log.warn("撤销水票通过orderGoodsId兜底反查失败 - tenantId={}, shopOrderId={}", tenantId, shopOrderId, e);
}
}
if (tickets == null || tickets.isEmpty()) {
return 0;
}
LocalDateTime now = LocalDateTime.now();
int revoked = 0;
// 不强制覆盖 comments避免影响后台人工备注reason 仅用于日志。
String reasonForLog = StrUtil.isBlank(reason) ? "订单取消/退款撤销水票" : reason.trim();
// 去重(避免 orderId/orderNo 与 orderGoodsId 两种路径重复命中)
Set<Integer> seen = new HashSet<>();
for (GltUserTicket t : tickets) {
if (t == null || t.getId() == null) {
continue;
}
if (!seen.add(t.getId())) {
continue;
}
Integer userTicketId = t.getId();
// 1) 删除未完成的送水订单(避免继续配送/接单/确认)
try {
LambdaUpdateWrapper<GltTicketOrder> uw = new LambdaUpdateWrapper<GltTicketOrder>()
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.eq(GltTicketOrder::getUserTicketId, userTicketId)
// 兼容历史/脏数据deliveryStatus 为空时也按“未完成”处理
.and(w -> w.ne(GltTicketOrder::getDeliveryStatus, TICKET_ORDER_DELIVERY_STATUS_FINISHED)
.or().isNull(GltTicketOrder::getDeliveryStatus))
.set(GltTicketOrder::getDeleted, 1)
.set(GltTicketOrder::getUpdateTime, now);
gltTicketOrderService.update(null, uw);
} catch (Exception e) {
log.warn("撤销送水订单失败(继续尝试撤销水票/释放计划) - tenantId={}, shopOrderId={}, userTicketId={}",
tenantId, shopOrderId, userTicketId, e);
}
// 2) 删除未完成的释放计划(防止后续继续自动释放)
try {
LambdaUpdateWrapper<GltUserTicketRelease> uw = new LambdaUpdateWrapper<GltUserTicketRelease>()
.eq(GltUserTicketRelease::getTenantId, tenantId)
.eq(GltUserTicketRelease::getDeleted, 0)
.eq(GltUserTicketRelease::getUserTicketId, userTicketId.longValue())
// status 为空时也视为“未完成”
.and(w -> w.ne(GltUserTicketRelease::getStatus, RELEASE_STATUS_DONE)
.or().isNull(GltUserTicketRelease::getStatus))
.set(GltUserTicketRelease::getDeleted, 1)
.set(GltUserTicketRelease::getUpdateTime, now);
gltUserTicketReleaseService.update(null, uw);
} catch (Exception e) {
log.warn("撤销水票释放计划失败(继续尝试撤销水票) - tenantId={}, shopOrderId={}, userTicketId={}",
tenantId, shopOrderId, userTicketId, e);
}
// 3) 撤销水票本身(软删除;幂等)
boolean ok = gltUserTicketService.update(
null,
new LambdaUpdateWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.eq(GltUserTicket::getId, userTicketId)
.set(GltUserTicket::getDeleted, 1)
.set(GltUserTicket::getUpdateTime, now)
);
if (ok) {
revoked++;
}
}
log.info("撤销水票完成 - tenantId={}, shopOrderId={}, shopOrderNo={}, tickets={}, reason={}",
tenantId, shopOrderId, shopOrderNo, revoked, reasonForLog);
return revoked;
}
}

View File

@@ -54,6 +54,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
private static final BigDecimal RIDER_UNIT_COMMISSION = new BigDecimal("0.10"); private static final BigDecimal RIDER_UNIT_COMMISSION = new BigDecimal("0.10");
private static final int RIDER_COMMISSION_SCALE = 2; private static final int RIDER_COMMISSION_SCALE = 2;
private static final int TENANT_ID_10584 = 10584; private static final int TENANT_ID_10584 = 10584;
private static final DateTimeFormatter SEND_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Resource @Resource
private GltUserTicketMapper gltUserTicketMapper; private GltUserTicketMapper gltUserTicketMapper;
@@ -155,6 +156,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
gltTicketOrder.setStatus(0); gltTicketOrder.setStatus(0);
gltTicketOrder.setDeleted(0); gltTicketOrder.setDeleted(0);
gltTicketOrder.setTenantId(tenantId); gltTicketOrder.setTenantId(tenantId);
// 关联商城订单号(用于后台/对账/追踪);优先取水票上的 orderNo缺失则按 orderId/orderGoodsId 兜底反查。
gltTicketOrder.setOrderNo(resolveShopOrderNo(userTicket, tenantId));
if (gltTicketOrder.getDeliveryStatus() == null) { if (gltTicketOrder.getDeliveryStatus() == null) {
gltTicketOrder.setDeliveryStatus(DELIVERY_STATUS_WAITING); gltTicketOrder.setDeliveryStatus(DELIVERY_STATUS_WAITING);
} }
@@ -164,6 +167,10 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (gltTicketOrder.getCreateTime() == null) { if (gltTicketOrder.getCreateTime() == null) {
gltTicketOrder.setCreateTime(now); gltTicketOrder.setCreateTime(now);
} }
// “立刻送水”下单场景不再需要前端选择配送时间;若未传则默认当前时间,便于排序与派单。
if (!StringUtils.hasText(gltTicketOrder.getSendTime())) {
gltTicketOrder.setSendTime(now.format(SEND_TIME_FMT));
}
gltTicketOrder.setUpdateTime(now); gltTicketOrder.setUpdateTime(now);
if (!this.save(gltTicketOrder)) { if (!this.save(gltTicketOrder)) {
throw new BusinessException("创建订单失败"); throw new BusinessException("创建订单失败");
@@ -203,6 +210,43 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
return gltTicketOrder; return gltTicketOrder;
} }
private String resolveShopOrderNo(GltUserTicket userTicket, Integer tenantId) {
if (userTicket == null || tenantId == null) {
return null;
}
if (StringUtils.hasText(userTicket.getOrderNo())) {
return userTicket.getOrderNo();
}
Integer orderId = userTicket.getOrderId();
// 兜底:历史数据可能只写了 orderGoodsId未写 orderId/orderNo
if (orderId == null && userTicket.getOrderGoodsId() != null) {
ShopOrderGoods og = shopOrderGoodsService.getOne(
new LambdaQueryWrapper<ShopOrderGoods>()
.select(ShopOrderGoods::getOrderId)
.eq(ShopOrderGoods::getTenantId, tenantId)
.eq(ShopOrderGoods::getId, userTicket.getOrderGoodsId())
.last("limit 1")
);
if (og != null) {
orderId = og.getOrderId();
}
}
if (orderId == null) {
return null;
}
ShopOrder order = shopOrderService.getOne(
new LambdaQueryWrapper<ShopOrder>()
.select(ShopOrder::getOrderNo)
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderId, orderId)
.last("limit 1")
);
return order == null ? null : order.getOrderNo();
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void accept(Integer id, Integer riderId, Integer tenantId) { public void accept(Integer id, Integer riderId, Integer tenantId) {

View File

@@ -115,7 +115,7 @@ public class DealerCommissionUnfreeze10584Task {
private final AtomicBoolean running = new AtomicBoolean(false); private final AtomicBoolean running = new AtomicBoolean(false);
@Scheduled(cron = "${dealer.commission.unfreeze10584.cron:0/30 * * * * ?}") @Scheduled(cron = "${dealer.commission.unfreeze10584.cron:0/20 * * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤") @IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() { public void run() {
if (!running.compareAndSet(false, true)) { if (!running.compareAndSet(false, true)) {

View File

@@ -3,6 +3,8 @@ package com.gxwebsoft.glt.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.common.core.annotation.IgnoreTenant; import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.glt.entity.GltTicketTemplate;
import com.gxwebsoft.glt.service.GltTicketTemplateService;
import com.gxwebsoft.shop.entity.ShopDealerCapital; import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerOrder; import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.entity.ShopDealerReferee; import com.gxwebsoft.shop.entity.ShopDealerReferee;
@@ -87,6 +89,9 @@ public class DealerOrderSettlement10584Task {
@Resource @Resource
private UserMapper userMapper; private UserMapper userMapper;
@Resource
private GltTicketTemplateService gltTicketTemplateService;
/** /**
* 每10秒执行一次。 * 每10秒执行一次。
*/ */
@@ -94,7 +99,8 @@ public class DealerOrderSettlement10584Task {
@IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常") @IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常")
public void settleTenant10584Orders() { public void settleTenant10584Orders() {
try { try {
List<ShopOrder> orders = findUnsettledPaidOrders(); Set<Integer> waterFormIds = loadWaterFormIds();
List<ShopOrder> orders = findUnsettledPaidOrders(waterFormIds);
if (orders.isEmpty()) { if (orders.isEmpty()) {
return; return;
} }
@@ -105,7 +111,7 @@ public class DealerOrderSettlement10584Task {
DealerBasicSetting dealerBasicSetting = findDealerBasicSetting(); DealerBasicSetting dealerBasicSetting = findDealerBasicSetting();
ShopDealerUser totalDealerUser = findTotalDealerUser(); ShopDealerUser totalDealerUser = findTotalDealerUser();
if (totalDealerUser == null || totalDealerUser.getUserId() == null) { if (totalDealerUser == null || totalDealerUser.getUserId() == null) {
log.warn("未找到总经销商账号,订单仍可结算但不会发放总经销商分润 - tenantId={}", TENANT_ID); log.warn("未找到分红账号,订单仍可结算但不会发放分红 - tenantId={}", TENANT_ID);
} }
log.debug("租户{}分销设置 - level={}", TENANT_ID, dealerBasicSetting.level); log.debug("租户{}分销设置 - level={}", TENANT_ID, dealerBasicSetting.level);
@@ -118,7 +124,7 @@ public class DealerOrderSettlement10584Task {
try { try {
transactionTemplate.executeWithoutResult(status -> { transactionTemplate.executeWithoutResult(status -> {
// 先“认领”订单:并发/多实例下避免重复结算update=0 表示被其他线程/实例处理) // 先“认领”订单:并发/多实例下避免重复结算update=0 表示被其他线程/实例处理)
if (!claimOrderToSettle(order.getOrderId())) { if (!claimOrderToSettle(order.getOrderId(), waterFormIds)) {
return; return;
} }
settleOneOrder(order, level1ParentCache, shopRoleCache, totalDealerUser, dealerBasicSetting.level); settleOneOrder(order, level1ParentCache, shopRoleCache, totalDealerUser, dealerBasicSetting.level);
@@ -132,28 +138,61 @@ public class DealerOrderSettlement10584Task {
} }
} }
private List<ShopOrder> findUnsettledPaidOrders() { private List<ShopOrder> findUnsettledPaidOrders(Set<Integer> waterFormIds) {
// 以确认收货为准:仅结算 deliveryStatus=20 的订单(租户10584约定)。 // 租户10584约定
return shopOrderService.list( // - 普通订单以发货为准deliveryStatus=20才结算
new LambdaQueryWrapper<ShopOrder>() // - 绑定水票模板的订单:支付成功即可体现分润(无需等发货状态变更)。
LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, TENANT_ID) .eq(ShopOrder::getTenantId, TENANT_ID)
.eq(ShopOrder::getDeleted, 0) .eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getPayStatus, true) .eq(ShopOrder::getPayStatus, true)
.eq(ShopOrder::getDeliveryStatus, 20)
.eq(ShopOrder::getIsSettled, 0) .eq(ShopOrder::getIsSettled, 0)
.orderByAsc(ShopOrder::getOrderId) // 退款/取消订单不结算,避免“退款后仍发放分红/分润/佣金”
.last("limit " + MAX_ORDERS_PER_RUN) .and(w -> w.notIn(ShopOrder::getOrderStatus, 2, 4, 5, 6, 7).or().isNull(ShopOrder::getOrderStatus));
);
if (waterFormIds != null && !waterFormIds.isEmpty()) {
qw.and(w -> w.eq(ShopOrder::getDeliveryStatus, 20).or().in(ShopOrder::getFormId, waterFormIds));
} else {
qw.eq(ShopOrder::getDeliveryStatus, 20);
} }
private boolean claimOrderToSettle(Integer orderId) { qw.orderByAsc(ShopOrder::getOrderId).last("limit " + MAX_ORDERS_PER_RUN);
return shopOrderService.update( return shopOrderService.list(qw);
new LambdaUpdateWrapper<ShopOrder>() }
private boolean claimOrderToSettle(Integer orderId, Set<Integer> waterFormIds) {
LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getOrderId, orderId) .eq(ShopOrder::getOrderId, orderId)
.eq(ShopOrder::getTenantId, TENANT_ID) .eq(ShopOrder::getTenantId, TENANT_ID)
.eq(ShopOrder::getIsSettled, 0) .eq(ShopOrder::getIsSettled, 0)
.set(ShopOrder::getIsSettled, 1) // 二次防御:退款/取消订单不允许被“认领结算”
); .and(w -> w.notIn(ShopOrder::getOrderStatus, 2, 4, 5, 6, 7).or().isNull(ShopOrder::getOrderStatus));
if (waterFormIds != null && !waterFormIds.isEmpty()) {
uw.and(w -> w.eq(ShopOrder::getDeliveryStatus, 20).or().in(ShopOrder::getFormId, waterFormIds));
} else {
uw.eq(ShopOrder::getDeliveryStatus, 20);
}
uw.set(ShopOrder::getIsSettled, 1);
return shopOrderService.update(uw);
}
private Set<Integer> loadWaterFormIds() {
try {
return gltTicketTemplateService.list(
new LambdaQueryWrapper<GltTicketTemplate>()
.eq(GltTicketTemplate::getTenantId, TENANT_ID)
.eq(GltTicketTemplate::getDeleted, 0)
.isNotNull(GltTicketTemplate::getGoodsId)
).stream()
.map(GltTicketTemplate::getGoodsId)
.filter(Objects::nonNull)
.collect(java.util.stream.Collectors.toSet());
} catch (Exception e) {
log.warn("读取水票模板goodsId失败将按普通订单规则结算 - tenantId={}", TENANT_ID, e);
return Collections.emptySet();
}
} }
private void settleOneOrder( private void settleOneOrder(
@@ -201,10 +240,10 @@ public class DealerOrderSettlement10584Task {
// 1) 直推/间推shop_dealer_referee // 1) 直推/间推shop_dealer_referee
DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount, goodsQty, commissionConfig, dealerLevel); DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount, goodsQty, commissionConfig, dealerLevel);
// 2) 门店分上级:从下单用户开始逐级向上找,命中 ShopDealerUser.type=1 的最近两级(直推门店/间推门店)。 // 2) 门店分上级:从下单用户开始逐级向上找,命中 ShopDealerUser.type=1 的最近两级(直推门店/间推门店)。
ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount, goodsQty, commissionConfig, level1ParentCache, shopRoleCache); ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount, goodsQty, commissionConfig, level1ParentCache, shopRoleCache);
// 3) 总经销商分润:固定比率,每个订单都分。 // 3) 分红:固定比率,每个订单都分。
TotalDealerCommission totalDealerCommission = settleTotalDealerCommission(order, baseAmount, goodsQty, totalDealerUser); TotalDealerCommission totalDealerCommission = settleTotalDealerCommission(order, baseAmount, goodsQty, totalDealerUser);
// 4) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准) // 4) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准)
@@ -265,7 +304,7 @@ public class DealerOrderSettlement10584Task {
directMoney, directMoney,
order, order,
order.getUserId(), order.getUserId(),
buildCommissionComment("直推佣金", commissionConfig.commissionType, commissionConfig.dealerDirectValue, goodsQty) buildCommissionComment("分佣", commissionConfig.commissionType, commissionConfig.dealerDirectValue, goodsQty)
); );
} }
if (normalizedLevel >= 2) { if (normalizedLevel >= 2) {
@@ -320,7 +359,7 @@ public class DealerOrderSettlement10584Task {
Map<Integer, Boolean> shopRoleCache Map<Integer, Boolean> shopRoleCache
) { ) {
List<Integer> shopRoleReferees = findFirstTwoShopRoleReferees(order.getUserId(), level1ParentCache, shopRoleCache); List<Integer> shopRoleReferees = findFirstTwoShopRoleReferees(order.getUserId(), level1ParentCache, shopRoleCache);
log.info("门店分命中结果(type=1门店角色取前两级) - orderNo={}, buyerUserId={}, shopRoleReferees={}", log.info("门店分命中结果(type=1门店角色取前两级) - orderNo={}, buyerUserId={}, shopRoleReferees={}",
order.getOrderNo(), order.getUserId(), shopRoleReferees); order.getOrderNo(), order.getUserId(), shopRoleReferees);
if (shopRoleReferees.isEmpty()) { if (shopRoleReferees.isEmpty()) {
return ShopRoleCommission.empty(); return ShopRoleCommission.empty();
@@ -330,14 +369,14 @@ public class DealerOrderSettlement10584Task {
// 仅找到一个门店:按(直推+间推)汇总发放 // 仅找到一个门店:按(直推+间推)汇总发放
BigDecimal singleStoreValue = safeValue(commissionConfig.storeDirectValue).add(safeValue(commissionConfig.storeSimpleValue)); BigDecimal singleStoreValue = safeValue(commissionConfig.storeDirectValue).add(safeValue(commissionConfig.storeSimpleValue));
BigDecimal money = calcMoneyByCommissionType(baseAmount, singleStoreValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType); BigDecimal money = calcMoneyByCommissionType(baseAmount, singleStoreValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType);
log.info("发放(仅1门店) - orderNo={}, firstDividendUserId={}, commissionType={}, value={}, money={}", log.info("发放(仅1门店) - orderNo={}, firstDividendUserId={}, commissionType={}, value={}, money={}",
order.getOrderNo(), shopRoleReferees.get(0), commissionConfig.commissionType, singleStoreValue, money); order.getOrderNo(), shopRoleReferees.get(0), commissionConfig.commissionType, singleStoreValue, money);
creditDealerCommission( creditDealerCommission(
shopRoleReferees.get(0), shopRoleReferees.get(0),
money, money,
order, order,
order.getUserId(), order.getUserId(),
buildCommissionComment("门店直推佣金(仅1门店)", commissionConfig.commissionType, singleStoreValue, goodsQty) buildCommissionComment("门店直推分润(仅1门店)", commissionConfig.commissionType, singleStoreValue, goodsQty)
); );
return new ShopRoleCommission(shopRoleReferees.get(0), money, null, BigDecimal.ZERO); return new ShopRoleCommission(shopRoleReferees.get(0), money, null, BigDecimal.ZERO);
} }
@@ -347,7 +386,7 @@ public class DealerOrderSettlement10584Task {
calcMoneyByCommissionType(baseAmount, commissionConfig.storeDirectValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType); calcMoneyByCommissionType(baseAmount, commissionConfig.storeDirectValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType);
BigDecimal storeSimpleMoney = BigDecimal storeSimpleMoney =
calcMoneyByCommissionType(baseAmount, commissionConfig.storeSimpleValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType); calcMoneyByCommissionType(baseAmount, commissionConfig.storeSimpleValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType);
log.info("发放(2人) - orderNo={}, firstDividendUserId={}, commissionType={}, firstValue={}, firstMoney={}, secondDividendUserId={}, secondValue={}, secondMoney={}", log.info("发放(2人) - orderNo={}, firstDividendUserId={}, commissionType={}, firstValue={}, firstMoney={}, secondDividendUserId={}, secondValue={}, secondMoney={}",
order.getOrderNo(), order.getOrderNo(),
shopRoleReferees.get(0), shopRoleReferees.get(0),
commissionConfig.commissionType, commissionConfig.commissionType,
@@ -361,14 +400,14 @@ public class DealerOrderSettlement10584Task {
storeDirectMoney, storeDirectMoney,
order, order,
order.getUserId(), order.getUserId(),
buildCommissionComment("门店直推佣金", commissionConfig.commissionType, commissionConfig.storeDirectValue, goodsQty) buildCommissionComment("门店直推分润", commissionConfig.commissionType, commissionConfig.storeDirectValue, goodsQty)
); );
creditDealerCommission( creditDealerCommission(
shopRoleReferees.get(1), shopRoleReferees.get(1),
storeSimpleMoney, storeSimpleMoney,
order, order,
order.getUserId(), order.getUserId(),
buildCommissionComment("门店间推佣金", commissionConfig.commissionType, commissionConfig.storeSimpleValue, goodsQty) buildCommissionComment("门店间推分润", commissionConfig.commissionType, commissionConfig.storeSimpleValue, goodsQty)
); );
return new ShopRoleCommission(shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney); return new ShopRoleCommission(shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney);
} }
@@ -387,14 +426,14 @@ public class DealerOrderSettlement10584Task {
rate = TOTAL_DEALER_DIVIDEND_RATE; rate = TOTAL_DEALER_DIVIDEND_RATE;
} }
BigDecimal money = calcMoneyByCommissionType(baseAmount, rate, goodsQty, DIVIDEND_SCALE, 20); BigDecimal money = calcMoneyByCommissionType(baseAmount, rate, goodsQty, DIVIDEND_SCALE, 20);
log.info("总经销商分润发放 - orderNo={}, totalDealerUserId={}, rate={}, money={}", log.info("分红发放 - orderNo={}, totalDealerUserId={}, rate={}, money={}",
order.getOrderNo(), totalDealerUser.getUserId(), rate, money); order.getOrderNo(), totalDealerUser.getUserId(), rate, money);
creditDealerCommission( creditDealerCommission(
totalDealerUser.getUserId(), totalDealerUser.getUserId(),
money, money,
order, order,
order.getUserId(), order.getUserId(),
buildCommissionComment("总经销商分润", 20, rate, goodsQty) buildCommissionComment("分红", 20, rate, goodsQty)
); );
return new TotalDealerCommission(totalDealerUser.getUserId(), money); return new TotalDealerCommission(totalDealerUser.getUserId(), money);
} }
@@ -451,7 +490,7 @@ public class DealerOrderSettlement10584Task {
} }
/** /**
* 门店分规则: * 门店分规则:
* - 门店角色为 ShopDealerUser.type=1 * - 门店角色为 ShopDealerUser.type=1
* - 从下单用户开始,沿 shop_dealer_referee(level=1) 链路逐级向上找; * - 从下单用户开始,沿 shop_dealer_referee(level=1) 链路逐级向上找;
* - 遇到第一个 type=1 用户命中为“直推门店用户”,继续向上找到第二个 type=1 用户命中为“间推门店用户”。 * - 遇到第一个 type=1 用户命中为“直推门店用户”,继续向上找到第二个 type=1 用户命中为“间推门店用户”。
@@ -641,7 +680,7 @@ public class DealerOrderSettlement10584Task {
.last("limit 1") .last("limit 1")
); );
if (existed != null) { if (existed != null) {
// 允许“补发”门店分时回填分字段,避免订单已结算但分字段一直为空,影响排查/对账。 // 允许“补发”门店分时回填分字段,避免订单已结算但分字段一直为空,影响排查/对账。
LambdaUpdateWrapper<ShopDealerOrder> uw = new LambdaUpdateWrapper<ShopDealerOrder>() LambdaUpdateWrapper<ShopDealerOrder> uw = new LambdaUpdateWrapper<ShopDealerOrder>()
.eq(ShopDealerOrder::getTenantId, TENANT_ID) .eq(ShopDealerOrder::getTenantId, TENANT_ID)
.eq(ShopDealerOrder::getOrderNo, order.getOrderNo()); .eq(ShopDealerOrder::getOrderNo, order.getOrderNo());
@@ -676,7 +715,7 @@ public class DealerOrderSettlement10584Task {
} }
if (needUpdate) { if (needUpdate) {
shopDealerOrderService.update(uw); shopDealerOrderService.update(uw);
log.info("ShopDealerOrder已存在回填门店分字段 - orderNo={}, firstDividendUser={}, secondDividendUser={}", log.info("ShopDealerOrder已存在回填门店分字段 - orderNo={}, firstDividendUser={}, secondDividendUser={}",
order.getOrderNo(), shopRoleCommission.storeDirectUserId, shopRoleCommission.storeSimpleUserId); order.getOrderNo(), shopRoleCommission.storeDirectUserId, shopRoleCommission.storeSimpleUserId);
} else { } else {
log.info("ShopDealerOrder已存在跳过写入 - orderNo={}", order.getOrderNo()); log.info("ShopDealerOrder已存在跳过写入 - orderNo={}", order.getOrderNo());
@@ -697,7 +736,7 @@ public class DealerOrderSettlement10584Task {
dealerOrder.setThirdUserId(dealerRefereeCommission.thirdDealerId); dealerOrder.setThirdUserId(dealerRefereeCommission.thirdDealerId);
dealerOrder.setThirdMoney(dealerRefereeCommission.thirdMoney); dealerOrder.setThirdMoney(dealerRefereeCommission.thirdMoney);
// 门店(角色shop)两级分单独落字段(详细以 ShopDealerCapital 为准) // 门店(角色shop)两级分单独落字段(详细以 ShopDealerCapital 为准)
dealerOrder.setFirstDividendUser(shopRoleCommission.storeDirectUserId); dealerOrder.setFirstDividendUser(shopRoleCommission.storeDirectUserId);
dealerOrder.setFirstDividend(shopRoleCommission.storeDirectMoney); dealerOrder.setFirstDividend(shopRoleCommission.storeDirectMoney);
dealerOrder.setSecondDividendUser(shopRoleCommission.storeSimpleUserId); dealerOrder.setSecondDividendUser(shopRoleCommission.storeSimpleUserId);

View File

@@ -18,7 +18,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* GLT 套票发放任务: * GLT 套票发放任务:
* - 每30秒扫描一次今日订单tenantId=10584, formId in 套票模板 goodsId, payStatus=1, orderStatus=0 * - 每30秒扫描一次今日订单tenantId=10584, formId in 套票模板 goodsId, payStatus=1, orderStatus=0
* - 为订单生成用户套票账户 + 释放计划(幂等) * - 为订单生成用户套票账户 + 释放计划(幂等)
* - 若模板配置了 startSendQty则发放时自动核销对应数量用于“第一次送水”场景 * - 按整改需求:发放阶段不再自动核销/自动下单;“送水下单核销”由用户在履约时主动触发
*/ */
@Slf4j @Slf4j
@Component @Component

View File

@@ -16,6 +16,7 @@ import com.gxwebsoft.common.system.entity.Payment;
import com.gxwebsoft.shop.entity.ShopOrderDelivery; import com.gxwebsoft.shop.entity.ShopOrderDelivery;
import com.gxwebsoft.shop.entity.ShopUserAddress; import com.gxwebsoft.shop.entity.ShopUserAddress;
import com.gxwebsoft.shop.service.*; import com.gxwebsoft.shop.service.*;
import com.gxwebsoft.glt.service.GltTicketRevokeService;
import com.gxwebsoft.shop.service.impl.KuaiDi100Impl; import com.gxwebsoft.shop.service.impl.KuaiDi100Impl;
import com.gxwebsoft.shop.task.OrderAutoCancelTask; import com.gxwebsoft.shop.task.OrderAutoCancelTask;
import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.entity.ShopOrder;
@@ -102,6 +103,10 @@ public class ShopOrderController extends BaseController {
private PaymentService paymentService; private PaymentService paymentService;
@Resource @Resource
private ShopStoreFenceService shopStoreFenceService; private ShopStoreFenceService shopStoreFenceService;
@Resource
private GltTicketRevokeService gltTicketRevokeService;
@Resource
private ShopDealerCommissionRollbackService shopDealerCommissionRollbackService;
@Operation(summary = "分页查询订单") @Operation(summary = "分页查询订单")
@GetMapping("/page") @GetMapping("/page")
@@ -373,6 +378,21 @@ public class ShopOrderController extends BaseController {
} }
if (shopOrderService.updateById(shopOrder)) { if (shopOrderService.updateById(shopOrder)) {
// 后台手工将订单改为“已取消”(2)时,需同步撤销可能已发放的水票/释放计划/送水订单
try {
if (Objects.equals(shopOrder.getOrderStatus(), 2) && !Objects.equals(shopOrderNow.getOrderStatus(), 2)) {
gltTicketRevokeService.revokeByShopOrder(
ObjectUtil.defaultIfNull(shopOrder.getTenantId(), shopOrderNow.getTenantId()),
shopOrderNow.getOrderId(),
shopOrderNow.getOrderNo(),
"订单取消撤销水票"
);
}
} catch (Exception e) {
logger.error("订单更新为取消后撤销水票失败 - orderId={}, orderNo={}",
shopOrderNow.getOrderId(), shopOrderNow.getOrderNo(), e);
}
// 如果订单上带了快递单号(常见于后台手工修正/补录),同步到发货单表,避免发货单还是旧单号 // 如果订单上带了快递单号(常见于后台手工修正/补录),同步到发货单表,避免发货单还是旧单号
if (StrUtil.isNotBlank(shopOrder.getExpressNo()) && shopOrder.getOrderId() != null) { if (StrUtil.isNotBlank(shopOrder.getExpressNo()) && shopOrder.getOrderId() != null) {
try { try {
@@ -527,6 +547,38 @@ public class ShopOrderController extends BaseController {
return fail("退款成功,但订单状态更新失败,请联系管理员"); return fail("退款成功,但订单状态更新失败,请联系管理员");
} }
// 退款成功后撤销水票相关数据(幂等;无水票则无副作用)
try {
gltTicketRevokeService.revokeByShopOrder(
current.getTenantId(),
current.getOrderId(),
current.getOrderNo(),
"订单退款成功撤销水票"
);
} catch (Exception e) {
// 退款已完成,不能因为撤销失败而回滚;记录日志以便人工补偿
logger.error("退款成功但撤销水票失败 - tenantId={}, orderId={}, orderNo={}",
current.getTenantId(), current.getOrderId(), current.getOrderNo(), e);
}
// 退款成功后回退分红/分润/佣金(从 ShopDealerUser 中扣回;以 ShopDealerCapital 明细为准)
try {
Integer tenantId = ObjectUtil.defaultIfNull(current.getTenantId(), getTenantId());
ShopOrder rollbackOrder = new ShopOrder();
rollbackOrder.setTenantId(tenantId);
rollbackOrder.setOrderNo(current.getOrderNo());
rollbackOrder.setPayPrice(current.getPayPrice());
rollbackOrder.setTotalPrice(current.getTotalPrice());
boolean rollbackOk = shopDealerCommissionRollbackService.rollbackOnOrderRefund(rollbackOrder, refundAmount);
if (!rollbackOk) {
logger.error("退款成功但回退分红/分润/佣金失败 - tenantId={}, orderId={}, orderNo={}",
tenantId, current.getOrderId(), current.getOrderNo());
}
} catch (Exception e) {
logger.error("退款成功但回退分红/分润/佣金异常 - tenantId={}, orderId={}, orderNo={}",
current.getTenantId(), current.getOrderId(), current.getOrderNo(), e);
}
logger.info("订单退款请求成功 - 订单号: {}, 退款单号: {}, 微信退款单号: {}", logger.info("订单退款请求成功 - 订单号: {}, 退款单号: {}, 微信退款单号: {}",
current.getOrderNo(), refundNo, refundResponse.getTransactionId()); current.getOrderNo(), refundNo, refundResponse.getTransactionId());
return success("退款成功"); return success("退款成功");
@@ -568,7 +620,42 @@ public class ShopOrderController extends BaseController {
return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund"); return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund");
} }
} }
if (batchParam.update(shopOrderService, "order_id")) { boolean ok = batchParam.update(shopOrderService, "order_id");
if (ok) {
// 兼容后台直接将订单改为“已取消”(2)的场景:同步撤销可能已发放的水票/释放计划/送水订单
try {
if (batchParam != null && batchParam.getData() != null
&& Objects.equals(batchParam.getData().getOrderStatus(), 2)
&& batchParam.getIds() != null && !batchParam.getIds().isEmpty()) {
for (Object rawId : batchParam.getIds()) {
Integer orderId = null;
if (rawId instanceof Integer) {
orderId = (Integer) rawId;
} else if (rawId != null) {
try {
orderId = Integer.valueOf(rawId.toString());
} catch (Exception ignore) {
// ignore malformed id
}
}
if (orderId == null) {
continue;
}
ShopOrder order = shopOrderService.getById(orderId);
if (order == null) {
continue;
}
gltTicketRevokeService.revokeByShopOrder(
order.getTenantId(),
order.getOrderId(),
order.getOrderNo(),
"订单取消撤销水票"
);
}
}
} catch (Exception e) {
logger.error("批量取消订单后撤销水票失败", e);
}
return success("修改成功"); return success("修改成功");
} }
return fail("修改失败"); return fail("修改失败");

View File

@@ -0,0 +1,72 @@
package com.gxwebsoft.shop.controller;
import cn.hutool.core.util.ObjectUtil;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.shop.dto.RefundedOrderGltRepairRequest;
import com.gxwebsoft.shop.service.ShopOrderGltRepairService;
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.HashMap;
import java.util.Map;
/**
* 历史订单修复:用于对“已退款的旧订单”补偿撤销水票相关数据。
*/
@Tag(name = "订单修复")
@RestController
@RequestMapping("/api/shop/shop-order/repair")
public class ShopOrderGltRepairController extends BaseController {
@Resource
private ShopOrderGltRepairService shopOrderGltRepairService;
@PreAuthorize("hasAuthority('shop:shopOrder:manage')")
@Operation(summary = "修复:已退款旧订单补偿撤销水票/释放计划/送水单支持dryRun预览")
@PostMapping("/revoke-glt-after-refund")
public ApiResult<?> revokeGltAfterRefund(@RequestBody RefundedOrderGltRepairRequest req) {
if (req == null) {
return fail("请求体不能为空");
}
Integer tenantId = ObjectUtil.defaultIfNull(req.getTenantId(), getTenantId());
if (tenantId == null) {
return fail("tenantId不能为空");
}
boolean dryRun = req.getDryRun() == null || req.getDryRun();
// 防误操作:既未指定订单,也未指定时间窗口时,只允许 dryRun
boolean noTargets = (req.getOrderIds() == null || req.getOrderIds().isEmpty())
&& (req.getOrderNos() == null || req.getOrderNos().isEmpty())
&& req.getRefundTimeStart() == null
&& req.getRefundTimeEnd() == null;
if (noTargets && !dryRun) {
return fail("请指定 orderIds/orderNos 或 refundTimeStart/refundTimeEnd否则仅允许 dryRun=true 预览");
}
ShopOrderGltRepairService.RepairResult r = shopOrderGltRepairService.revokeTicketsForRefundedOrders(
tenantId,
req.getOrderIds(),
req.getOrderNos(),
req.getRefundTimeStart(),
req.getRefundTimeEnd(),
req.getBatchSize(),
dryRun
);
Map<String, Object> data = new HashMap<>();
data.put("tenantId", tenantId);
data.put("dryRun", r.dryRun());
data.put("scannedOrders", r.scannedOrders());
data.put("ordersWithTicketsRevoked", r.ordersWithTicketsRevoked());
data.put("revokedTicketCount", r.revokedTicketCount());
data.put("processedOrderIds", r.processedOrderIds());
return success(data);
}
}

View File

@@ -0,0 +1,41 @@
package com.gxwebsoft.shop.dto;
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;
import java.util.List;
/**
* 历史退款订单:补偿撤销水票/释放计划/送水单 的修复请求。
*/
@Data
public class RefundedOrderGltRepairRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "租户ID不传则使用当前请求租户")
private Integer tenantId;
@Schema(description = "指定订单ID列表优先使用为空则走时间窗口扫描")
private List<Integer> orderIds;
@Schema(description = "指定订单号列表(可选;为空则走时间窗口扫描)")
private List<String> orderNos;
@Schema(description = "退款时间起yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime refundTimeStart;
@Schema(description = "退款时间止yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime refundTimeEnd;
@Schema(description = "每次处理条数上限默认200")
private Integer batchSize;
@Schema(description = "是否仅预览默认true仅统计不落库")
private Boolean dryRun;
}

View File

@@ -37,6 +37,10 @@ public class ShopDealerCapital implements Serializable {
@Schema(description = "订单编号") @Schema(description = "订单编号")
private String orderNo; private String orderNo;
@Schema(description = "订单状态")
@TableField(exist = false)
private Integer orderStatus;
@Schema(description = "资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入 50佣金解冻 60配送奖励)") @Schema(description = "资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入 50佣金解冻 60配送奖励)")
private Integer flowType; private Integer flowType;

View File

@@ -4,11 +4,12 @@
<!-- 关联查询sql --> <!-- 关联查询sql -->
<sql id="selectSql"> <sql id="selectSql">
SELECT a.*, b.order_no, b.month, c.nickname AS nickName, d.nickname AS toNickName SELECT a.*, b.order_no, b.month, c.nickname AS nickName, d.nickname AS toNickName, e.order_status AS orderStatus
FROM shop_dealer_capital a FROM shop_dealer_capital a
LEFT JOIN shop_dealer_order b ON a.order_no = b.order_no LEFT JOIN shop_dealer_order b ON a.order_no = b.order_no
LEFT JOIN gxwebsoft_core.sys_user c ON a.user_id = c.user_id and c.deleted = 0 LEFT JOIN gxwebsoft_core.sys_user c ON a.user_id = c.user_id and c.deleted = 0
LEFT JOIN gxwebsoft_core.sys_user d ON a.to_user_id = d.user_id and d.deleted = 0 LEFT JOIN gxwebsoft_core.sys_user d ON a.to_user_id = d.user_id and d.deleted = 0
LEFT JOIN shop_order e ON a.order_no = e.order_no
<where> <where>
<if test="param.id != null"> <if test="param.id != null">
AND a.id = #{param.id} AND a.id = #{param.id}

View File

@@ -56,7 +56,8 @@
AND a.create_time &lt;= #{param.createTimeEnd} AND a.create_time &lt;= #{param.createTimeEnd}
</if> </if>
<if test="param.keywords != null"> <if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') AND (a.name LIKE CONCAT('%', #{param.keywords}, '%')
OR a.phone LIKE CONCAT('%', #{param.keywords}, '%')
) )
</if> </if>
</where> </where>

View File

@@ -0,0 +1,21 @@
package com.gxwebsoft.shop.service;
import com.gxwebsoft.shop.entity.ShopOrder;
import java.math.BigDecimal;
/**
* 分销/分红/分润:订单退款回退
*/
public interface ShopDealerCommissionRollbackService {
/**
* 订单退款成功后,按订单号回退已入账(冻结/可提现)的分销佣金/分红/分润等金额。
*
* @param order 订单(必须包含 tenantId、orderNo
* @param refundAmount 退款金额(允许为空;为空则按全额退款处理)
* @return true=执行成功或无可回退数据false=执行失败
*/
boolean rollbackOnOrderRefund(ShopOrder order, BigDecimal refundAmount);
}

View File

@@ -0,0 +1,109 @@
package com.gxwebsoft.shop.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.glt.service.GltTicketRevokeService;
import com.gxwebsoft.shop.entity.ShopOrder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 历史订单修复:对已退款订单补偿撤销水票相关数据。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ShopOrderGltRepairService {
/** shop_order.order_status退款成功 */
private static final int ORDER_STATUS_REFUND_SUCCESS = 6;
private final ShopOrderService shopOrderService;
private final GltTicketRevokeService gltTicketRevokeService;
public RepairResult revokeTicketsForRefundedOrders(Integer tenantId,
List<Integer> orderIds,
List<String> orderNos,
LocalDateTime refundTimeStart,
LocalDateTime refundTimeEnd,
Integer batchSize,
boolean dryRun) {
if (tenantId == null) {
throw new IllegalArgumentException("tenantId不能为空");
}
int limit = batchSize == null ? 200 : Math.max(1, Math.min(batchSize, 2000));
List<ShopOrder> orders = new ArrayList<>();
// 1) 精准修复:指定 orderIds / orderNos
if (orderIds != null && !orderIds.isEmpty()) {
orders = shopOrderService.list(new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderStatus, ORDER_STATUS_REFUND_SUCCESS)
.in(ShopOrder::getOrderId, orderIds)
.last("limit " + limit));
} else if (orderNos != null && !orderNos.isEmpty()) {
orders = shopOrderService.list(new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderStatus, ORDER_STATUS_REFUND_SUCCESS)
.in(ShopOrder::getOrderNo, orderNos)
.last("limit " + limit));
} else {
// 2) 扫描修复:按 refundTime 窗口分页处理
LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderStatus, ORDER_STATUS_REFUND_SUCCESS);
if (refundTimeStart != null) {
qw.ge(ShopOrder::getRefundTime, refundTimeStart);
}
if (refundTimeEnd != null) {
qw.le(ShopOrder::getRefundTime, refundTimeEnd);
}
orders = shopOrderService.list(qw.orderByAsc(ShopOrder::getRefundTime).orderByAsc(ShopOrder::getOrderId).last("limit " + limit));
}
int scanned = orders == null ? 0 : orders.size();
int revokedTickets = 0;
int revokedOrders = 0;
List<Integer> processedOrderIds = new ArrayList<>();
if (orders != null) {
for (ShopOrder o : orders) {
if (o == null || o.getOrderId() == null) {
continue;
}
processedOrderIds.add(o.getOrderId());
if (dryRun) {
continue;
}
int revoked = gltTicketRevokeService.revokeByShopOrder(
tenantId,
o.getOrderId(),
o.getOrderNo(),
"历史退款订单补偿撤销水票"
);
if (revoked > 0) {
revokedTickets += revoked;
revokedOrders++;
}
}
}
return new RepairResult(scanned, revokedOrders, revokedTickets, processedOrderIds, dryRun);
}
public record RepairResult(int scannedOrders,
int ordersWithTicketsRevoked,
int revokedTicketCount,
List<Integer> processedOrderIds,
boolean dryRun) {
}
}

View File

@@ -0,0 +1,189 @@
package com.gxwebsoft.shop.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
import com.gxwebsoft.shop.service.ShopDealerCommissionRollbackService;
import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.service.ShopDealerUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class ShopDealerCommissionRollbackServiceImpl implements ShopDealerCommissionRollbackService {
private static final int FLOW_TYPE_COMMISSION_INCOME = 10;
private static final int FLOW_TYPE_COMMISSION_UNFREEZE_MARKER = 50;
private static final int FLOW_TYPE_DELIVERY_REWARD = 60;
@Resource
private ShopDealerCapitalService shopDealerCapitalService;
@Resource
private ShopDealerUserService shopDealerUserService;
@Resource
private ShopDealerOrderService shopDealerOrderService;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean rollbackOnOrderRefund(ShopOrder order, BigDecimal refundAmount) {
if (order == null || order.getTenantId() == null || order.getOrderNo() == null || order.getOrderNo().isBlank()) {
return true;
}
Integer tenantId = order.getTenantId();
String orderNo = order.getOrderNo();
BigDecimal orderBaseAmount = ObjectUtil.defaultIfNull(order.getPayPrice(), order.getTotalPrice());
BigDecimal ratio = resolveRefundRatio(orderBaseAmount, refundAmount);
List<ShopDealerCapital> capitals = shopDealerCapitalService.list(
new LambdaQueryWrapper<ShopDealerCapital>()
.eq(ShopDealerCapital::getTenantId, tenantId)
.eq(ShopDealerCapital::getOrderNo, orderNo)
.in(ShopDealerCapital::getFlowType, FLOW_TYPE_COMMISSION_INCOME, FLOW_TYPE_DELIVERY_REWARD)
.isNotNull(ShopDealerCapital::getUserId)
.isNotNull(ShopDealerCapital::getMoney)
.gt(ShopDealerCapital::getMoney, BigDecimal.ZERO)
);
if (capitals == null || capitals.isEmpty()) {
// 仍标记分销订单失效,避免后续统计误判
markDealerOrderInvalid(tenantId, orderNo);
return true;
}
Map<Integer, BigDecimal> freezeDeductByUser = new HashMap<>();
Map<Integer, BigDecimal> moneyDeductByUser = new HashMap<>();
Map<Integer, BigDecimal> totalDeductByUser = new HashMap<>();
for (ShopDealerCapital cap : capitals) {
Integer dealerUserId = cap.getUserId();
BigDecimal amount = cap.getMoney();
if (dealerUserId == null || amount == null || amount.signum() <= 0) {
continue;
}
BigDecimal rollbackAmount = amount.multiply(ratio).setScale(2, RoundingMode.HALF_UP);
if (rollbackAmount.signum() <= 0) {
continue;
}
totalDeductByUser.merge(dealerUserId, rollbackAmount, BigDecimal::add);
Integer flowType = cap.getFlowType();
if (flowType != null && flowType == FLOW_TYPE_DELIVERY_REWARD) {
moneyDeductByUser.merge(dealerUserId, rollbackAmount, BigDecimal::add);
continue;
}
// 佣金收入:若已解冻(有 flowType=50 marker),则从可提现扣回;否则从冻结扣回
boolean unfrozen = hasUnfreezeMarker(tenantId, cap);
if (unfrozen) {
moneyDeductByUser.merge(dealerUserId, rollbackAmount, BigDecimal::add);
} else {
freezeDeductByUser.merge(dealerUserId, rollbackAmount, BigDecimal::add);
}
}
LocalDateTime now = LocalDateTime.now();
for (Map.Entry<Integer, BigDecimal> entry : totalDeductByUser.entrySet()) {
Integer dealerUserId = entry.getKey();
BigDecimal totalDeduct = entry.getValue();
if (dealerUserId == null || totalDeduct == null || totalDeduct.signum() <= 0) {
continue;
}
BigDecimal freezeDeduct = freezeDeductByUser.getOrDefault(dealerUserId, BigDecimal.ZERO);
BigDecimal moneyDeduct = moneyDeductByUser.getOrDefault(dealerUserId, BigDecimal.ZERO);
LambdaUpdateWrapper<ShopDealerUser> uw = new LambdaUpdateWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, tenantId)
.eq(ShopDealerUser::getUserId, dealerUserId)
.setSql("total_money = IFNULL(total_money,0) - " + totalDeduct.toPlainString())
.set(ShopDealerUser::getUpdateTime, now);
if (freezeDeduct.signum() > 0) {
uw.setSql("freeze_money = IFNULL(freeze_money,0) - " + freezeDeduct.toPlainString());
}
if (moneyDeduct.signum() > 0) {
uw.setSql("money = IFNULL(money,0) - " + moneyDeduct.toPlainString());
}
boolean updated = shopDealerUserService.update(uw);
if (!updated) {
log.warn("订单退款扣回分销金额失败:未找到分销账户 - tenantId={}, orderNo={}, dealerUserId={}, totalDeduct={}, freezeDeduct={}, moneyDeduct={}",
tenantId, orderNo, dealerUserId, totalDeduct, freezeDeduct, moneyDeduct);
}
}
markDealerOrderInvalid(tenantId, orderNo);
return true;
}
private boolean hasUnfreezeMarker(Integer tenantId, ShopDealerCapital cap) {
if (tenantId == null || cap == null || cap.getId() == null || cap.getUserId() == null || cap.getOrderNo() == null) {
return false;
}
String markerComment = buildUnfreezeMarkerComment(cap.getId());
return shopDealerCapitalService.count(
new LambdaQueryWrapper<ShopDealerCapital>()
.eq(ShopDealerCapital::getTenantId, tenantId)
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_COMMISSION_UNFREEZE_MARKER)
.eq(ShopDealerCapital::getUserId, cap.getUserId())
.eq(ShopDealerCapital::getOrderNo, cap.getOrderNo())
.eq(ShopDealerCapital::getComments, markerComment)
) > 0;
}
private String buildUnfreezeMarkerComment(Integer capitalId) {
return "佣金解冻(capitalId=" + capitalId + ")";
}
private BigDecimal resolveRefundRatio(BigDecimal orderBaseAmount, BigDecimal refundAmount) {
if (refundAmount == null || refundAmount.signum() <= 0) {
return BigDecimal.ONE;
}
if (orderBaseAmount == null || orderBaseAmount.signum() <= 0) {
return BigDecimal.ONE;
}
if (refundAmount.compareTo(orderBaseAmount) >= 0) {
return BigDecimal.ONE;
}
return refundAmount.divide(orderBaseAmount, 10, RoundingMode.HALF_UP);
}
private void markDealerOrderInvalid(Integer tenantId, String orderNo) {
if (tenantId == null || orderNo == null || orderNo.isBlank()) {
return;
}
try {
shopDealerOrderService.update(
new LambdaUpdateWrapper<ShopDealerOrder>()
.eq(ShopDealerOrder::getTenantId, tenantId)
.eq(ShopDealerOrder::getOrderNo, orderNo)
.set(ShopDealerOrder::getIsInvalid, 1)
.set(ShopDealerOrder::getUpdateTime, LocalDateTime.now())
);
} catch (Exception e) {
log.warn("订单退款标记分销订单失效失败 - tenantId={}, orderNo={}", tenantId, orderNo, e);
}
}
}

View File

@@ -38,7 +38,7 @@ public class OrderAutoCancelTask {
* 生产环境每5分钟执行一次 * 生产环境每5分钟执行一次
* 开发环境每1分钟执行一次便于测试 * 开发环境每1分钟执行一次便于测试
*/ */
@Scheduled(cron = "${shop.order.auto-cancel.cron:0 */5 * * * ?}") @Scheduled(cron = "${shop.order.auto-cancel.cron:0 */1 * * * ?}")
@IgnoreTenant("定时任务需要处理所有租户的超时订单") @IgnoreTenant("定时任务需要处理所有租户的超时订单")
public void cancelExpiredOrders() { public void cancelExpiredOrders() {
if (!orderConfig.getAutoCancel().isEnabled()) { if (!orderConfig.getAutoCancel().isEnabled()) {

View File

@@ -0,0 +1,86 @@
# 服务器配置
server:
port: 9300
# 数据源配置
spring:
datasource:
url: jdbc:mysql://1Panel-mysql-XsWW:3306/gltdb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: gltdb
password: EeD4FtzyA5ksj7Bk
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
remove-abandoned: true
# redis
redis:
database: 0
host: 1Panel-redis-GmNr
port: 6379
password: redis_t74P8C
# 日志配置
logging:
file:
name: websoft-modules.log
level:
root: WARN
com.gxwebsoft: ERROR
com.baomidou.mybatisplus: ERROR
socketio:
host: 0.0.0.0 #IP地址
# MQTT配置
mqtt:
enabled: false # 启用MQTT服务
host: tcp://132.232.214.96:1883
username: swdev
password: Sw20250523
client-id-prefix: hjm_car_
topic: /SW_GPS/#
qos: 2
connection-timeout: 10
keep-alive-interval: 20
auto-reconnect: true
# 框架配置
config:
# 文件服务器
file-server: https://file-s209.shoplnk.cn
# 生产环境接口
server-url: https://glt-server.websoft.top/api
# 业务模块接口
api-url: https://glt-api.websoft.top/api
upload-path: /www/wwwroot/file.ws
# 阿里云OSS云存储
endpoint: https://oss-cn-shenzhen.aliyuncs.com
accessKeyId: LTAI4GKGZ9Z2Z8JZ77c3GNZP
accessKeySecret: BiDkpS7UXj72HWwDWaFZxiXjNFBNCM
bucketName: oss-gxwebsoft
bucketDomain: https://oss.wsdns.cn
aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com
# 生产环境证书配置
certificate:
load-mode: VOLUME # 生产环境从Docker挂载卷加载
cert-root-path: /www/wwwroot/file.ws
# 支付配置缓存
payment:
cache:
# 支付配置缓存键前缀,生产环境使用 Payment:1* 格式
key-prefix: "Payment:1"
# 缓存过期时间(小时)
expire-hours: 24
# 阿里云翻译配置
aliyun:
translate:
access-key-id: LTAI5tEsyhW4GCKbds1qsopg
access-key-secret: zltFlQrYVAoq2KMFDWgLa3GhkMNeyO
endpoint: mt.cn-hangzhou.aliyuncs.com
wechatpay:
transfer:
scene-id: 1005
scene-report-infos-json: '[{"info_type":"岗位类型","info_content":"配送员"},{"info_type":"报酬说明","info_content":"12月份配送费"}]'

View File

@@ -1,4 +1,4 @@
# 生产环境配置 # 服务器配置
server: server:
port: 9200 port: 9200
@@ -22,12 +22,9 @@ spring:
# 日志配置 # 日志配置
logging: logging:
file:
name: websoft-modules.log
level: level:
root: WARN com.gxwebsoft: DEBUG
com.gxwebsoft: ERROR com.baomidou.mybatisplus: DEBUG
com.baomidou.mybatisplus: ERROR
socketio: socketio:
host: 0.0.0.0 #IP地址 host: 0.0.0.0 #IP地址

View File

@@ -4,7 +4,7 @@ server:
# 多环境配置 # 多环境配置
spring: spring:
profiles: profiles:
active: glt2 active: ysb2
application: application:
name: server name: server

View File

@@ -2,9 +2,17 @@ package com.gxwebsoft;
import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.service.GltTicketRevokeService;
import com.gxwebsoft.glt.service.GltUserTicketService;
import com.gxwebsoft.hjm.controller.PushCallback; import com.gxwebsoft.hjm.controller.PushCallback;
import com.gxwebsoft.hjm.entity.HjmCar; import com.gxwebsoft.hjm.entity.HjmCar;
import com.gxwebsoft.hjm.service.HjmCarService; import com.gxwebsoft.hjm.service.HjmCarService;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.entity.ShopOrderGoods;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -14,6 +22,9 @@ import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.text.ParseException; import java.text.ParseException;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/** /**
* Created by WebSoft on 2020-03-23 23:37 * Created by WebSoft on 2020-03-23 23:37
@@ -25,6 +36,131 @@ public class TestMain {
@Resource @Resource
private HjmCarService hjmCarService; private HjmCarService hjmCarService;
@Resource
private ShopOrderService shopOrderService;
@Resource
private ShopOrderGoodsService shopOrderGoodsService;
@Resource
private GltUserTicketService gltUserTicketService;
@Resource
private GltTicketRevokeService gltTicketRevokeService;
/**
* 查询已退款订单ShopOrder关联的gltUserTicket仅处理租户ID=10584
*/
@Test
public void testOrderData() {
final int tenantId = 10584;
final int ORDER_STATUS_REFUND_SUCCESS = 6;
// 安全开关:默认只查询/打印;如需实际撤销,将其改为 true
final boolean doRevoke = false;
int lastOrderId = 0;
int batchSize = 200;
int scannedOrders = 0;
int ordersWithTickets = 0;
int matchedTickets = 0;
int revokedTickets = 0;
while (true) {
List<ShopOrder> orders = shopOrderService.list(new LambdaQueryWrapper<ShopOrder>()
.select(ShopOrder::getOrderId, ShopOrder::getOrderNo, ShopOrder::getRefundTime, ShopOrder::getRefundMoney,
ShopOrder::getOrderStatus, ShopOrder::getPayStatus, ShopOrder::getCreateTime)
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderStatus, ORDER_STATUS_REFUND_SUCCESS)
.gt(ShopOrder::getOrderId, lastOrderId)
.orderByAsc(ShopOrder::getOrderId)
.last("limit " + batchSize));
if (orders == null || orders.isEmpty()) {
break;
}
for (ShopOrder o : orders) {
if (o == null || o.getOrderId() == null) {
continue;
}
scannedOrders++;
lastOrderId = Math.max(lastOrderId, o.getOrderId());
List<GltUserTicket> tickets = findTicketsByOrder(tenantId, o.getOrderId(), o.getOrderNo());
if (tickets == null || tickets.isEmpty()) {
tickets = findTicketsByOrderGoodsFallback(tenantId, o.getOrderId());
}
if (tickets == null || tickets.isEmpty()) {
continue;
}
ordersWithTickets++;
matchedTickets += tickets.size();
logger.info("已退款订单关联水票 - tenantId={}, orderId={}, orderNo={}, refundMoney={}, refundTime={}, tickets={}",
tenantId, o.getOrderId(), o.getOrderNo(), o.getRefundMoney(), o.getRefundTime(),
tickets.stream().filter(Objects::nonNull).map(GltUserTicket::getId).collect(Collectors.toList()));
if (doRevoke) {
int revoked = gltTicketRevokeService.revokeByShopOrder(
tenantId,
o.getOrderId(),
o.getOrderNo(),
"TestMain: 历史退款订单撤销水票"
);
revokedTickets += revoked;
}
}
}
logger.info("扫描完成 - tenantId={}, scannedOrders={}, ordersWithTickets={}, matchedTickets={}, doRevoke={}, revokedTickets={}",
tenantId, scannedOrders, ordersWithTickets, matchedTickets, doRevoke, revokedTickets);
}
private List<GltUserTicket> findTicketsByOrder(Integer tenantId, Integer orderId, String orderNo) {
LambdaQueryWrapper<GltUserTicket> qw = new LambdaQueryWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0);
if (orderId != null && StrUtil.isNotBlank(orderNo)) {
qw.and(w -> w.eq(GltUserTicket::getOrderId, orderId).or().eq(GltUserTicket::getOrderNo, orderNo));
} else if (orderId != null) {
qw.eq(GltUserTicket::getOrderId, orderId);
} else if (StrUtil.isNotBlank(orderNo)) {
qw.eq(GltUserTicket::getOrderNo, orderNo);
} else {
return List.of();
}
return gltUserTicketService.list(qw);
}
/**
* 兼容历史数据:部分水票仅记录了 order_goods_id未回填 order_id/order_no。
*/
private List<GltUserTicket> findTicketsByOrderGoodsFallback(Integer tenantId, Integer orderId) {
if (tenantId == null || orderId == null) {
return List.of();
}
List<ShopOrderGoods> goods = shopOrderGoodsService.list(new LambdaQueryWrapper<ShopOrderGoods>()
.select(ShopOrderGoods::getId)
.eq(ShopOrderGoods::getTenantId, tenantId)
.eq(ShopOrderGoods::getOrderId, orderId));
if (goods == null || goods.isEmpty()) {
return List.of();
}
List<Integer> orderGoodsIds = goods.stream()
.map(ShopOrderGoods::getId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (orderGoodsIds.isEmpty()) {
return List.of();
}
return gltUserTicketService.list(new LambdaQueryWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.in(GltUserTicket::getOrderGoodsId, orderGoodsIds));
}
/** /**
* 生成唯一的key用于jwt工具类 * 生成唯一的key用于jwt工具类
*/ */

View File

@@ -0,0 +1,105 @@
package com.gxwebsoft.credit.controller;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.credit.entity.CreditBankruptcy;
import com.gxwebsoft.credit.service.CreditBankruptcyService;
import com.gxwebsoft.credit.service.CreditCompanyRecordCountService;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CreditBankruptcyImportSheetSelectionTest {
@Test
void import_should_prefer_sheet_named_bankruptcy_reorganization() throws Exception {
MultipartFile file = buildWorkbookWithTwoSheets();
CreditBankruptcyController controller = new CreditBankruptcyController();
CreditBankruptcyService creditBankruptcyService = Mockito.mock(CreditBankruptcyService.class);
BatchImportSupport batchImportSupport = Mockito.mock(BatchImportSupport.class);
CreditCompanyRecordCountService recordCountService = Mockito.mock(CreditCompanyRecordCountService.class);
// Capture inserted entities; controller clears the chunk list after calling persistInsertOnlyChunk.
List<CreditBankruptcy> inserted = new ArrayList<>();
Mockito.when(batchImportSupport.persistInsertOnlyChunk(
Mockito.eq(creditBankruptcyService),
Mockito.anyList(),
Mockito.anyList(),
Mockito.anyInt(),
Mockito.any(),
Mockito.anyString(),
Mockito.anyList()
))
.thenAnswer(invocation -> {
@SuppressWarnings("unchecked")
List<CreditBankruptcy> items = new ArrayList<>((List<CreditBankruptcy>) invocation.getArgument(1));
inserted.addAll(items);
return items.size();
});
ReflectionTestUtils.setField(controller, "creditBankruptcyService", creditBankruptcyService);
ReflectionTestUtils.setField(controller, "batchImportSupport", batchImportSupport);
ReflectionTestUtils.setField(controller, "creditCompanyRecordCountService", recordCountService);
ApiResult<List<String>> result = controller.importBatch(file, null);
assertNotNull(result);
assertEquals(0, result.getCode());
assertTrue(result.getMessage().contains("成功导入1条"), "message=" + result.getMessage());
assertEquals(1, inserted.size());
// "历史破产重整" sheet is first; we should import from "破产重整" instead.
assertEquals("R1", inserted.get(0).getCode());
}
private static MultipartFile buildWorkbookWithTwoSheets() throws Exception {
try (Workbook workbook = new XSSFWorkbook()) {
// Put "历史破产重整" first to ensure old readAnySheet() behavior would import the wrong sheet.
writeBankruptcySheet(workbook.createSheet("历史破产重整"), "H1");
writeBankruptcySheet(workbook.createSheet("破产重整"), "R1");
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
workbook.write(bos);
return new MockMultipartFile(
"file",
"bankruptcy.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
bos.toByteArray()
);
}
}
}
private static void writeBankruptcySheet(Sheet sheet, String code) {
// Keep it simple: a single header row + single data row.
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("案号");
header.createCell(1).setCellValue("案件类型");
header.createCell(2).setCellValue("当事人");
header.createCell(3).setCellValue("经办法院");
header.createCell(4).setCellValue("公开日期");
header.createCell(5).setCellValue("备注");
Row row = sheet.createRow(1);
row.createCell(0).setCellValue(code);
row.createCell(1).setCellValue("破产重整");
row.createCell(2).setCellValue("示例公司");
row.createCell(3).setCellValue("示例法院");
row.createCell(4).setCellValue("2026-01-01");
row.createCell(5).setCellValue("备注");
}
}

View File

@@ -0,0 +1,381 @@
package com.gxwebsoft.generator;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.gxwebsoft.generator.engine.BeetlTemplateEnginePlus;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* CMS模块-代码生成工具
*
* @author WebSoft
* @since 2021-09-05 00:31:14
*/
public class YsbGenerator {
// 输出位置
private static final String OUTPUT_LOCATION = System.getProperty("user.dir");
//private static final String OUTPUT_LOCATION = "D:/codegen"; // 不想生成到项目中可以写磁盘路径
// JAVA输出目录
private static final String OUTPUT_DIR = "/src/main/java";
// Vue文件输出位置
private static final String OUTPUT_LOCATION_VUE = "/Users/gxwebsoft/VUE/mp-vue";
// UniApp文件输出目录
private static final String OUTPUT_LOCATION_UNIAPP = "/Users/gxwebsoft/VUE/template-10579";
// Vue文件输出目录
private static final String OUTPUT_DIR_VUE = "/src";
// 作者名称
private static final String AUTHOR = "科技小王子";
// 是否在xml中添加二级缓存配置
private static final boolean ENABLE_CACHE = false;
// 数据库连接配置
private static final String DB_URL = "jdbc:mysql://47.119.165.234:13308/ysb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8";
private static final String DB_DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String DB_USERNAME = "ysb";
private static final String DB_PASSWORD = "5Zf45CE2YneBfXkR";
// 包名
private static final String PACKAGE_NAME = "com.gxwebsoft";
// 模块名
private static final String MODULE_NAME = "credit";
// 需要生成的表
private static final String[] TABLE_NAMES = new String[]{
"credit_mp_customer",
};
// 需要去除的表前缀
private static final String[] TABLE_PREFIX = new String[]{
"tb_"
};
// 不需要作为查询参数的字段
private static final String[] PARAM_EXCLUDE_FIELDS = new String[]{
"tenant_id",
"create_time",
"update_time"
};
// 查询参数使用String的类型
private static final String[] PARAM_TO_STRING_TYPE = new String[]{
"Date",
"LocalDate",
"LocalTime",
"LocalDateTime"
};
// 查询参数使用EQ的类型
private static final String[] PARAM_EQ_TYPE = new String[]{
"Integer",
"Boolean",
"BigDecimal"
};
// 是否添加权限注解
private static final boolean AUTH_ANNOTATION = true;
// 是否添加日志注解
private static final boolean LOG_ANNOTATION = true;
// controller的mapping前缀
private static final String CONTROLLER_MAPPING_PREFIX = "/api";
// 模板所在位置
private static final String TEMPLATES_DIR = "/src/test/java/com/gxwebsoft/generator/templates";
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
gc.setOutputDir(OUTPUT_LOCATION + OUTPUT_DIR);
gc.setAuthor(AUTHOR);
gc.setOpen(false);
gc.setFileOverride(true);
gc.setEnableCache(ENABLE_CACHE);
gc.setSwagger2(true);
gc.setIdType(IdType.AUTO);
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl(DB_URL);
// dsc.setSchemaName("public");
dsc.setDriverName(DB_DRIVER);
dsc.setUsername(DB_USERNAME);
dsc.setPassword(DB_PASSWORD);
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(MODULE_NAME);
pc.setParent(PACKAGE_NAME);
mpg.setPackageInfo(pc);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setInclude(TABLE_NAMES);
strategy.setTablePrefix(TABLE_PREFIX);
strategy.setSuperControllerClass(PACKAGE_NAME + ".common.core.web.BaseController");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setControllerMappingHyphenStyle(true);
strategy.setLogicDeleteFieldName("deleted");
mpg.setStrategy(strategy);
// 模板配置
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setController(TEMPLATES_DIR + "/controller.java");
templateConfig.setEntity(TEMPLATES_DIR + "/entity.java");
templateConfig.setMapper(TEMPLATES_DIR + "/mapper.java");
templateConfig.setXml(TEMPLATES_DIR + "/mapper.xml");
templateConfig.setService(TEMPLATES_DIR + "/service.java");
templateConfig.setServiceImpl(TEMPLATES_DIR + "/serviceImpl.java");
mpg.setTemplate(templateConfig);
mpg.setTemplateEngine(new BeetlTemplateEnginePlus());
// 自定义模板配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
Map<String, Object> map = new HashMap<>();
map.put("packageName", PACKAGE_NAME);
map.put("paramExcludeFields", PARAM_EXCLUDE_FIELDS);
map.put("paramToStringType", PARAM_TO_STRING_TYPE);
map.put("paramEqType", PARAM_EQ_TYPE);
map.put("authAnnotation", AUTH_ANNOTATION);
map.put("logAnnotation", LOG_ANNOTATION);
map.put("controllerMappingPrefix", CONTROLLER_MAPPING_PREFIX);
// 添加项目类型标识,用于模板中的条件判断
map.put("isUniApp", false); // Vue 项目
map.put("isVueAdmin", true); // 后台管理项目
this.setMap(map);
}
};
String templatePath = TEMPLATES_DIR + "/param.java.btl";
List<FileOutConfig> focList = new ArrayList<>();
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION + OUTPUT_DIR + "/"
+ PACKAGE_NAME.replace(".", "/")
+ "/" + pc.getModuleName() + "/param/"
+ tableInfo.getEntityName() + "Param" + StringPool.DOT_JAVA;
}
});
/**
* 以下是生成VUE项目代码
* 生成文件的路径 /api/shop/goods/index.ts
*/
templatePath = TEMPLATES_DIR + "/index.ts.btl";
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_VUE + OUTPUT_DIR_VUE
+ "/api/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/" + "index.ts";
}
});
// UniApp 使用专门的模板
String uniappTemplatePath = TEMPLATES_DIR + "/index.ts.uniapp.btl";
focList.add(new FileOutConfig(uniappTemplatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE
+ "/api/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/" + "index.ts";
}
});
// 生成TS文件 (/api/shop/goods/model/index.ts)
templatePath = TEMPLATES_DIR + "/model.ts.btl";
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_VUE + OUTPUT_DIR_VUE
+ "/api/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/model/" + "index.ts";
}
});
// UniApp 使用专门的 model 模板
String uniappModelTemplatePath = TEMPLATES_DIR + "/model.ts.uniapp.btl";
focList.add(new FileOutConfig(uniappModelTemplatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE
+ "/api/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/model/" + "index.ts";
}
});
// 生成Vue文件(/views/shop/goods/index.vue)
templatePath = TEMPLATES_DIR + "/index.vue.btl";
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_VUE + OUTPUT_DIR_VUE
+ "/views/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/" + "index.vue";
}
});
// 生成components文件(/views/shop/goods/components/edit.vue)
templatePath = TEMPLATES_DIR + "/components.edit.vue.btl";
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_VUE + OUTPUT_DIR_VUE
+ "/views/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/components/" + tableInfo.getEntityPath() + "Edit.vue";
}
});
// 生成components文件(/views/shop/goods/components/search.vue)
templatePath = TEMPLATES_DIR + "/components.search.vue.btl";
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_VUE + OUTPUT_DIR_VUE
+ "/views/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/components/" + "search.vue";
}
});
// ========== 移动端页面文件生成 ==========
// 生成移动端列表页面配置文件 (/src/shop/goods/index.config.ts)
templatePath = TEMPLATES_DIR + "/index.config.ts.btl";
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE
+ "/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/" + "index.config.ts";
}
});
// 生成移动端列表页面组件文件 (/src/shop/goods/index.tsx)
templatePath = TEMPLATES_DIR + "/index.tsx.btl";
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE
+ "/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/" + "index.tsx";
}
});
// 生成移动端新增/编辑页面配置文件 (/src/shop/goods/add.config.ts)
templatePath = TEMPLATES_DIR + "/add.config.ts.btl";
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE
+ "/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/" + "add.config.ts";
}
});
// 生成移动端新增/编辑页面组件文件 (/src/shop/goods/add.tsx)
templatePath = TEMPLATES_DIR + "/add.tsx.btl";
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
return OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE
+ "/" + pc.getModuleName() + "/"
+ tableInfo.getEntityPath() + "/" + "add.tsx";
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
mpg.execute();
// 自动更新 app.config.ts
updateAppConfig(TABLE_NAMES, MODULE_NAME);
}
/**
* 自动更新 app.config.ts 文件,添加新生成的页面路径
*/
private static void updateAppConfig(String[] tableNames, String moduleName) {
String appConfigPath = OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE + "/app.config.ts";
try {
// 读取原文件内容
String content = new String(Files.readAllBytes(Paths.get(appConfigPath)));
// 为每个表生成页面路径
StringBuilder newPages = new StringBuilder();
for (String tableName : tableNames) {
String entityPath = tableName.replaceAll("_", "");
// 转换为驼峰命名
String[] parts = tableName.split("_");
StringBuilder camelCase = new StringBuilder(parts[0]);
for (int i = 1; i < parts.length; i++) {
camelCase.append(parts[i].substring(0, 1).toUpperCase()).append(parts[i].substring(1));
}
entityPath = camelCase.toString();
newPages.append(" '").append(entityPath).append("/index',\n");
newPages.append(" '").append(entityPath).append("/add',\n");
}
// 查找对应模块的子包配置
String modulePattern = "\"root\":\\s*\"" + moduleName + "\",\\s*\"pages\":\\s*\\[([^\\]]*)]";
Pattern pattern = Pattern.compile(modulePattern, Pattern.DOTALL);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
String existingPages = matcher.group(1);
// 检查页面是否已存在,避免重复添加
boolean needUpdate = false;
String[] newPageArray = newPages.toString().split("\n");
for (String newPage : newPageArray) {
if (!newPage.trim().isEmpty() && !existingPages.contains(newPage.trim().replace(" ", "").replace(",", ""))) {
needUpdate = true;
break;
}
}
if (needUpdate) {
// 备份原文件
String backupPath = appConfigPath + ".backup." + System.currentTimeMillis();
Files.copy(Paths.get(appConfigPath), Paths.get(backupPath));
System.out.println("已备份原文件到: " + backupPath);
// 在现有页面列表末尾添加新页面
String updatedPages = existingPages.trim();
if (!updatedPages.endsWith(",")) {
updatedPages += ",";
}
updatedPages += "\n" + newPages.toString().trim();
// 替换内容
String updatedContent = content.replace(matcher.group(1), updatedPages);
// 写入更新后的内容
Files.write(Paths.get(appConfigPath), updatedContent.getBytes());
System.out.println("✅ 已自动更新 app.config.ts添加了以下页面路径:");
System.out.println(newPages.toString());
} else {
System.out.println(" app.config.ts 中已包含所有页面路径,无需更新");
}
} else {
System.out.println("⚠️ 未找到 " + moduleName + " 模块的子包配置,请手动添加页面路径");
}
} catch (Exception e) {
System.err.println("❌ 更新 app.config.ts 失败: " + e.getMessage());
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,69 @@
package com.gxwebsoft.glt.service;
import com.gxwebsoft.glt.entity.GltUserTicket;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GltTicketRevokeServiceTest {
@Mock
private GltUserTicketService gltUserTicketService;
@Mock
private GltUserTicketReleaseService gltUserTicketReleaseService;
@Mock
private GltTicketOrderService gltTicketOrderService;
@Mock
private com.gxwebsoft.shop.service.ShopOrderGoodsService shopOrderGoodsService;
@InjectMocks
private GltTicketRevokeService gltTicketRevokeService;
@Test
void revokeByShopOrder_noTenant_noop() {
int revoked = gltTicketRevokeService.revokeByShopOrder(null, 1, "O1", "r");
assertEquals(0, revoked);
verifyNoInteractions(gltUserTicketService, gltUserTicketReleaseService, gltTicketOrderService, shopOrderGoodsService);
}
@Test
void revokeByShopOrder_noTickets_noop() {
when(gltUserTicketService.list(any())).thenReturn(List.of());
when(shopOrderGoodsService.list(any())).thenReturn(List.of());
int revoked = gltTicketRevokeService.revokeByShopOrder(10584, 1, "O1", "r");
assertEquals(0, revoked);
verify(gltUserTicketService, times(1)).list(any());
verify(shopOrderGoodsService, times(1)).list(any());
verifyNoMoreInteractions(gltUserTicketService, shopOrderGoodsService);
verifyNoInteractions(gltUserTicketReleaseService, gltTicketOrderService);
}
@Test
void revokeByShopOrder_hasTickets_revokesAll() {
GltUserTicket t = new GltUserTicket();
t.setId(123);
t.setTenantId(10584);
when(gltUserTicketService.list(any())).thenReturn(List.of(t));
when(gltUserTicketService.update(isNull(), any())).thenReturn(true);
when(gltTicketOrderService.update(isNull(), any())).thenReturn(true);
when(gltUserTicketReleaseService.update(isNull(), any())).thenReturn(true);
int revoked = gltTicketRevokeService.revokeByShopOrder(10584, 1, "O1", "退款撤销");
assertEquals(1, revoked);
verify(gltTicketOrderService, times(1)).update(isNull(), any());
verify(gltUserTicketReleaseService, times(1)).update(isNull(), any());
verify(gltUserTicketService, times(1)).update(isNull(), any());
verifyNoInteractions(shopOrderGoodsService);
}
}

Binary file not shown.