Compare commits

..

266 Commits

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

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

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

- 将批量转账接口替换为商家转账(升级版)接口 /v3/fund-app/mch-transfer/transfer-bills
- 新增 transfer_scene_id 和场景报备信息配置支持
- 参数从 outBatchNo/outDetailNo 统一为 outBillNo 单号
- 添加商户单号长度限制校验(5-32字符)
- 支持接口路径fallback机制,兼容不同环境差异
- 实现转账场景报备信息的JSON配置解析功能
- 更新日志记录格式以匹配新接口响应结构
```
2026-01-29 02:39:24 +08:00
89177db718 fix(core): 修复LocalDateTime反序列化和微信支付参数验证问题
- 移除JacksonConfig中未使用的LocalDateTimeDeserializer导入
- 增强LocalDateTimeDeserializer支持时间戳格式解析,兼容前端发送的数字时间戳
- 添加构造函数支持自定义日期时间格式器
- 修复ShopDealerWithdrawController中微信支付批次号长度不足问题,使用零填充确保最小长度
- 添加微信支付服务中outBatchNo和outDetailNo参数长度验证规则
- 移除WxTransferService中的冗余代码行
2026-01-29 01:19:51 +08:00
d93dd04211 feat(withdraw): 实现分销商提现微信自动转账功能
- 新增 WxTransferService 服务类实现微信商家转账到零钱功能
- 在 ShopDealerWithdrawController 中集成微信转账服务
- 修改 update 方法支持微信收款方式的自动转账处理
- 添加事务管理确保转账操作的数据一致性
- 实现转账参数验证和错误处理机制
- 支持通过 openid 自动获取和用户姓名验证
- 添加转账金额转换和批次号生成逻辑
2026-01-29 00:28:47 +08:00
a3e812a9c4 feat(data): 更新公司记录计数服务并优化导入功能
- 在多个控制器中引入 CreditCompanyRecordCountService 依赖注入
- 添加 HashSet 和 Set 类型导入以支持公司ID集合操作
- 在Excel导入过程中跟踪受影响的公司ID集合
- 实现导入完成后批量刷新公司记录计数的功能
- 扩展 CreditCompany 实体类添加各类信用记录计数字段
- 优化导入逻辑确保公司记录计数实时更新
2026-01-28 21:46:45 +08:00
5e804bbf9a feat(data): 更新公司记录计数服务并优化导入功能
- 在多个控制器中引入 CreditCompanyRecordCountService 依赖注入
- 添加 HashSet 和 Set 类型导入以支持公司ID集合操作
- 在Excel导入过程中跟踪受影响的公司ID集合
- 实现导入完成后批量刷新公司记录计数的功能
- 扩展 CreditCompany 实体类添加各类信用记录计数字段
- 优化导入逻辑确保公司记录计数实时更新
2026-01-28 21:46:30 +08:00
ad2562c06e feat(order): 添加单一商品订单的formId更新逻辑
- 在OrderBusinessService中添加单一商品订单的formId设置功能
- 在ShopOrderServiceImpl中实现订单商品数量为1时的formId更新逻辑
- 确保只有当订单中只有一个商品时才更新formId字段
2026-01-28 17:13:32 +08:00
5a95375cda feat(settlement): 更新分销结算逻辑支持固定金额和百分比两种佣金类型
- 替换 CommissionRateConfig 为 CommissionConfig,支持 commissionType 字段区分固定金额和百分比模式
- 新增 calcMoneyByCommissionType 方法,根据 commissionType 计算佣金金额
- 修改 findOrderSingleGoods 为 findOrderSingleGoodsInfo,返回商品信息和数量
- 更新日志输出格式,显示商品数量和佣金类型信息
- 调整信用分销商佣金方法参数,传递商品数量和佣金配置对象
- 新增 OrderGoodsInfo 和 CommissionConfig 内部类定义
- 实现固定金额模式按件计算佣金的逻辑
- 添加安全数值处理方法 safeValue 和 safePositive
- 更新佣金注释构建方法,显示佣金类型和具体数值信息
2026-01-28 16:05:24 +08:00
9e5c5ceab3 feat(settlement): 更新分销结算逻辑支持固定金额和百分比两种佣金类型
- 替换 CommissionRateConfig 为 CommissionConfig,支持 commissionType 字段区分固定金额和百分比模式
- 新增 calcMoneyByCommissionType 方法,根据 commissionType 计算佣金金额
- 修改 findOrderSingleGoods 为 findOrderSingleGoodsInfo,返回商品信息和数量
- 更新日志输出格式,显示商品数量和佣金类型信息
- 调整信用分销商佣金方法参数,传递商品数量和佣金配置对象
- 新增 OrderGoodsInfo 和 CommissionConfig 内部类定义
- 实现固定金额模式按件计算佣金的逻辑
- 添加安全数值处理方法 safeValue 和 safePositive
- 更新佣金注释构建方法,显示佣金类型和具体数值信息
2026-01-28 15:11:22 +08:00
fa53fd399f feat(task): 完善经销商订单结算功能支持三级分销和商品级别配置
- 引入商品实体和订单商品实体依赖,新增相关服务注入
- 实现商品分销开关检查,未开启分销的商品跳过结算流程
- 添加三级分销佣金计算逻辑,支持第三级经销商佣金结算
- 实现商品级别的分润比例配置,支持按商品设置不同的佣金率
- 新增商品分销配置解析功能,兼容旧版固定比例逻辑
- 扩展分佣记录实体,增加第三级用户和金额字段
- 更新日志输出格式,显示详细的分润比例和金额信息
- 优化门店分红计算,支持单门店汇总分润和多门店分级分润
2026-01-28 14:49:49 +08:00
cbc9a1c861 fix(shop): 修复分销订单结算中的文案和逻辑错误
- 修复CmsOrderController中save方法的空行格式问题
- 将所有"简推"相关文案统一更正为"间推",包括注释和日志输出
- 修正ShopRoleCommission方法中的佣金类型描述从"简推"改为"间推"
- 修复门店分红规则描述中的术语一致性
- 修正ShopDealerOrderMapper.xml中的重复条件查询逻辑,
2026-01-28 14:00:20 +08:00
3814711ec4 feat(cms): 添加网站订单管理功能
- 创建 CmsOrder 实体类,包含订单基本信息、支付信息、物流信息等完整字段
- 实现 CmsOrderController 控制器,提供增删改查及批量操作接口
- 开发 CmsOrderMapper 数据访问层,集成 MyBatis-Plus 基础功能
- 配置 CmsOrderMapper.xml XML 映射文件,实现关联查询 SQL 语句
- 定义 CmsOrderParam 查询参数类,支持多条件动态查询
- 构建 CmsOrderService 业务接口及其实现类,封装订单业务逻辑
- 集成 Swagger 文档注解,提供 API 接口文档支持
- 添加权限控制注解,确保接口安全性
- 实现分页查询、列表查询、单条查询等多种数据获取方式
2026-01-27 13:22:37 +08:00
e4d4a19020 feat(settlement): 调整门店直推佣金比例并完善用户信息展示
- 将门店直推佣金从2%调整为3%(仅1门店情况)
- 新增RATE_0_03常量用于门店直推佣金计算
- 在ShopDealerCapital实体中添加分销商昵称字段
- 在ShopDealerOrder实体中添加门店名称字段
- 在ShopDealerUser实体中添加头像字段
- 更新Mapper XML文件以关联查询用户昵称和头像信息
- 修改日志输出信息以反映新的佣金比例和门店信息
2026-01-26 23:02:05 +08:00
3b63172012 refactor(task): 重构经销商订单结算任务中的上级用户查找逻辑
- 引入 UpstreamUserFinder 工具类来统一处理向上游用户链路的遍历逻辑
- 添加缓存机制减少数据库查询次数,提高性能
- 修改 settleOneOrder 方法签名以传递缓存对象
- 更新门店分红上级查找逻辑,从简单的链路取前两级改为精确查找门店角色用户
- 删除废弃的 ShopOrderSettlement10584Task 临时排查任务类
- 添加 UpstreamUserFinder 的单元测试确保逻辑正确性
2026-01-26 12:34:56 +08:00
803ac3301e refactor(task): 优化经销商订单结算任务的推荐关系查询逻辑
- 移除废弃的 isShopRole 扩展字段及相关 SQL 查询
- 将推荐关系查询逻辑改为基于 level=1 链路的简单向上遍历
- 添加分红金额计算精度控制,使用 3 位小数精度
- 调整定时任务执行频率从 30 秒改为 20 秒
- 优化订单结算时的分红用户和金额更新逻辑,支持增量更新
- 新增一次性排查任务用于调试订单推荐关系链路问题
2026-01-26 11:52:53 +08:00
375a65be6a refactor(task): 优化门店分佣逻辑按type字段判定
- 替换原有的角色shop判定逻辑,改为按ShopDealerUser.type=1判定门店分红用户
- 新增User和UserMapper依赖注入用于获取用户基础信息
- 添加幂等检查机制避免同一订单重复发放佣金
- 在创建分销商账户时补充基础信息防止字段约束导致插入失败
- 优化SQL查询逻辑,直接关联shop_dealer_user表而非查询系统角色表
- 更新日志信息中的描述文字以反映新的判定方式
- 添加对已存在订单记录的回填功能,支持补发门店分佣时更新分红字段
2026-01-26 01:01:13 +08:00
918190148b refactor(settlement): 优化经销商订单结算逻辑
- 移除不必要的UserRoleService依赖注入
- 将定时任务执行频率从每20秒调整为每30秒
- 删除过期的shopRoleCache缓存机制
- 重构推荐关系查询逻辑,支持多级关系和门店角色识别
- 添加对两种数据形态的兼容处理(level=1/2多级关系 vs 仅level=1关系)
- 修改佣金分配逻辑,区分直推佣金和推荐奖金
- 优化门店角色佣金计算,调整为直推2%和简推1%
- 添加分销商账户自动创建机制,确保佣金能够正常发放
- 更新资金明细记录,准确关联到对应的用户ID
- 新增自定义SQL查询方法,一次性获取推荐关系链路和门店角色信息
- 扩展ShopDealerReferee实体,增加isShopRole扩展字段
- 优化MyBatis XML映射,使用LEFT JOIN预加载角色信息避免N+1查询问题
2026-01-26 00:14:34 +08:00
d15cc03e48 feat(credit): 扩展信用实体并调整经销商结算逻辑
- 在CreditXgxf实体中添加原告/上诉人和被告/被上诉人字段
- 调整经销商订单结算任务中的佣金费率配置
- 修改简推佣金计算逻辑,允许同一个人获得双重佣金
- 更新门店推广佣金分配策略,第二名佣金从8%降至1%
- 扩展店铺经销商订单查询条件以支持更多用户层级
2026-01-25 12:08:36 +08:00
0035d3cd7d fix(search): 修复查询条件中的字段映射错误
- 移除无效的 code 字段查询条件
- 修正关键词搜索中错误的字段引用,将 code 替换为 case_number
- 清理多余的参数绑定表达式
2026-01-23 23:15:23 +08:00
08ab8da7d9 feat(order): 添加门店分红相关字段
- 添加门店(一级)字段 firstDividendUser
- 添加分红(一级)字段 firstDividend
- 添加门店(二级)字段 secondDividendUser
- 添加分红(二级)字段 secondDividend
- 为新增字段添加 Swagger 文档注解
- 保持原有分销佣金字段结构不变
2026-01-23 15:56:50 +08:00
06c20b8418 feat(settlement): 优化经销商订单结算任务并添加案件编号搜索功能
- 在多个信用查询映射文件中添加案件编号关键词搜索条件
- 在CreditGqdjMapper中添加代码字段搜索过滤器
- 修改结算任务逻辑,将void方法改为返回详细佣金信息的对象
- 添加经销商订单记录创建功能,用于结算追踪和统计
- 增加详细的日志记录便于调试和监控
- 添加防止重复结算的幂等功能
- 重构佣金计算逻辑,支持更精确的分佣跟踪
2026-01-23 14:11:28 +08:00
16e5e31f33 feat(credit): 添加数据类型字段支持
- 在CreditXgxf实体类中新增type字段并添加Swagger文档注解
- 在CreditXgxfController控制器中设置导入参数的type字段值
- 在CreditXgxfImportParam导入参数类中新增type字段并配置Excel映射
- 在CreditXgxfParam参数类中新增type字段并添加Swagger文档注解
2026-01-23 13:35:24 +08:00
afa5dd8ab2 feat(task): 添加租户10584分销订单结算定时任务
- 实现每20秒执行一次的定时任务,处理已付款且未结算的订单
- 添加直推和简推佣金计算逻辑,按10%比例发放
- 实现shop角色推荐人佣金分配,支持最多两级推荐
- 添加订单认领机制防止重复结算,并使用事务模板确保数据一致性
- 实现分销商账户余额累加和资金流水记录功能
- 添加缓存机制减少重复角色查询,提升性能
2026-01-23 00:22:54 +08:00
4ffc62fef1 feat(task): 添加租户10584分销订单结算定时任务
- 实现每20秒执行一次的定时任务,处理已付款且未结算的订单
- 添加直推和简推佣金计算逻辑,按10%比例发放
- 实现shop角色推荐人佣金分配,支持最多两级推荐
- 添加订单认领机制防止重复结算,并使用事务模板确保数据一致性
- 实现分销商账户余额累加和资金流水记录功能
- 添加缓存机制减少重复角色查询,提升性能
2026-01-23 00:20:36 +08:00
757291f256 config(application): 重命名测试配置文件并调整端口
- 将 application-test.yml 重命名为 application-cms.yml
- 将服务器端口从 9300 修改为 9100
- 在 ShopDealerReferee 实体类中为 source 和 scene 字段添加 @TableField(exist = false) 注解
- 移除 ShopDealerUserController 中 save 方法的权限验证注解和操作日志注解
2026-01-22 20:32:49 +08:00
f40010433d feat(credit): 添加主体企业名称关联查询功能
- 在多个实体类中添加companyName字段用于显示主体企业名称
- 修改XML映射文件实现与credit_company表的左连接查询
- 将企业名称作为companyName字段返回到查询结果中
- 修复CreditBranch实体类中字段描述的一致性问题
- 更新CreditNearbyCompany的关键词搜索逻辑以支持企业名称搜索
2026-01-22 08:51:32 +08:00
a6aba2c49b feat(shop): 添加客户编号查询功能
- 在 ShopDealerApplyParam 中新增 dealerCode 字段用于客户编号查询
- 在 ShopDealerApplyMapper.xml 中添加 dealer_code 查询条件
- 实现按客户编号精确匹配的查询逻辑
- 更新参数校验和文档注释
2026-01-21 17:40:51 +08:00
492928667a feat(shop): 添加分销商申请地址详情字段
- 在 ShopDealerApply 实体类中新增社区、楼栋号、单元号、房号字段
- 更新 ShopDealerApplyController 中的 Excel 导入功能,修改表头格式说明
- 在控制器中添加测试数据并完善实体对象设置逻辑
- 扩展 ShopDealerApplyImportParam 参数类,增加地址相关字段映射
- 更新 ShopDealerApplyMapper.xml 添加地址查询条件
- 在 ShopDealerApplyParam 查询参数类中添加地址过滤字段
- 统一多个信用模块的关键词搜索逻辑,从精确匹配改为模糊匹配
2026-01-21 17:34:47 +08:00
416027ffe8 feat(navigation): 添加网站导航导入参数类
- 创建 CmsNavigationImportParam 类用于 Excel 批量导入功能
- 定义导航ID、类型、菜单名称等基础字段映射
- 添加上级id、模型、标识等关联属性配置
- 集成路径、组件、打开位置等路由相关字段
- 包含图标、颜色、隐藏设置等UI显示属性
- 实现权限控制、访问密码等安全相关字段
- 添加位置、顶部底部显示等布局控制选项
- 配置活动路径、元信息、样式等扩展功能
- 整合模型名称、页面ID、详情页ID等数据关联
- 支持微信小程序菜单、间距宽度等特殊设置
- 包含阅读量、商户ID、语言等业务相关字段
- 添加设为首页、推荐、排序等管理功能
- 配置备注、状态、用户及租户ID等系统属性
- 使用 EasyPOI 注解实现 Excel 数据映射
- 继承 Serializable 接口支持序列化操作
2026-01-21 17:15:34 +08:00
30924cb7c3 feat(navigation): 添加网站导航导入参数类
- 创建 CmsNavigationImportParam 类用于 Excel 批量导入功能
- 定义导航ID、类型、菜单名称等基础字段映射
- 添加上级id、模型、标识等关联属性配置
- 集成路径、组件、打开位置等路由相关字段
- 包含图标、颜色、隐藏设置等UI显示属性
- 实现权限控制、访问密码
2026-01-21 13:18:13 +08:00
0104eccd34 feat(controller): 新增企业ID批量更新功能
- 在BatchImportSupport中添加CompanyIdRefreshStats统计类
- 实现基于企业名称匹配的companyId批量更新逻辑
- 添加normalizeCompanyName和addCompanyNameMapping辅助方法
- 在各个Credit控制器中注入CreditCompanyService依赖
- 为所有相关控制器添加/company-id/refresh接口端点
- 实现多租户环境下的安全匹配和更新机制
- 支持limit参数控制批量处理数量
- 提供详细的更新统计数据返回
2026-01-21 13:18:00 +08:00
7ba034ab1e feat(controller): 新增企业ID批量更新功能
- 在BatchImportSupport中添加CompanyIdRefreshStats统计类
- 实现基于企业名称匹配的companyId批量更新逻辑
- 添加normalizeCompanyName和addCompanyNameMapping辅助方法
- 在各个Credit控制器中注入CreditCompanyService依赖
- 为所有相关控制器添加/company-id/refresh接口端点
- 实现多租户环境下的安全匹配和更新机制
- 支持limit参数控制批量处理数量
- 提供详细的更新统计数据返回
2026-01-20 22:13:06 +08:00
15fc17e54b fix(credit): 修复债务人公司ID匹配逻辑
- 将默认更新条件从 companyId 为空改为 companyId=0
- 修改查询条件从 isNull 改为等于 0 的判断
- 更新业务逻辑中对 needUpdate 的判断条件

docs(shop): 添加经销商推荐绑定接口文档

- 新增 SHOP_DEALER_REFEREE_BINDING.md 文档
- 定义 POST /api/shop/shop-dealer-referee 接口规则
- 说明邀请人有效性验证和防止自绑限制
- 描述首次绑定幂等性和溯源字段要求
- 提供数据库唯一索引建议和建表语句
2026-01-20 17:25:28 +08:00
7487236ac6 feat(credit): 添加被执行人企业关联匹配功能
- 在 CreditJudgmentDebtorController 中新增 refreshCompanyIdByCompanyName 接口
- 实现根据企业名称自动匹配并更新 companyId 的批量处理逻辑
- 支持按租户维度进行企业名称匹配,避免跨租户误匹配
- 添加企业名称标准化处理和模糊匹配机制
- 实现批量更新和事务管理,提升处理效率
- 优化关键词搜索条件,精确匹配企业名称而非模糊匹配
- 添加公司名称规范化方法 normalizeCompanyName
- 修复竞品表字段别名从 mainCompanyName 改为 companyName

feat(shop): 优化分销商推荐关系绑定机制

- 修改 ShopDealerRefereeController 的 save 方法为幂等绑定
- 新增 bindFirstLevel 方法实现一级推荐关系的幂等绑定
- 添加用户身份验证和安全校验机制
- 增加 source 和 scene 字段支持来源追踪
- 实现重复绑定防护和业务异常处理
- 添加经销商有效性校验机制
2026-01-20 17:02:59 +08:00
1898d3ac9b refactor(user-card): 优化用户卡包统计数据获取逻辑
- 移除对UserService的依赖,改为使用UserCardStatsMapper直接查询
- 新增UserCardStatsMapper接口用于跨库查询用户余额和积分
- 添加MyBatis XML映射文件实现跨库查询gxwebsoft_core.sys_user表
- 实现类型转换工具方法toBigDecimal和toIntObj确保数据类型安全
- 修复因模块间依赖导致无法访问sys_user表的问题
- 保持租户隔离支持,确保数据安全性
2026-01-20 14:07:31 +08:00
8e5271ae38 feat(shop): 添加用户订单和卡包统计功能
- 在 ShopOrderMapper 中新增 selectUserOrderStats 方法用于订单状态统计
- 在 ShopOrderService 和 ShopOrderServiceImpl 中实现用户订单统计功能
- 添加 UserOrderStats DTO 类定义订单各状态数量统计
- 创建 UserOrderController 提供用户订单统计 API 接口
- 实现用户卡包统计功能,包括余额、积分、优惠券、礼品卡统计
- 添加 UserCardController 和 UserCardStats DTO 类
- 优化 Swagger 配置以支持 /api/user/** 路径的 API 文档
- 为统计接口添加 Redis 缓存以提升性能
- 清理 ShopOrderController 中不必要的导入依赖
2026-01-20 13:02:58 +08:00
b1b106c397 feat(shop): 添加用户订单和卡包统计功能
- 在 ShopOrderMapper 中新增 selectUserOrderStats 方法用于订单状态统计
- 在 ShopOrderService 和 ShopOrderServiceImpl 中实现用户订单统计功能
- 添加 UserOrderStats DTO 类定义订单各状态数量统计
- 创建 UserOrderController 提供用户订单统计 API 接口
- 实现用户卡包统计功能,包括余额、积分、优惠券、礼品卡统计
- 添加 UserCardController 和 UserCardStats DTO 类
- 优化 Swagger 配置以支持 /api/user/** 路径的 API 文档
- 为统计接口添加 Redis 缓存以提升性能
- 清理 ShopOrderController 中不必要的导入依赖
2026-01-20 13:00:41 +08:00
ceaaf287b0 feat(login): 添加微信登录中的应用运行状态检查功能
- 引入 CmsWebsite 和 CmsWebsiteService 依赖
- 注入 CmsWebsiteService 服务实例
- 实现基于租户ID的应用运行状态判断逻辑
- 根据运行状态动态设置页面检查路径和环境版本参数
- 当应用状态为试用模式时禁用路径检查并切换到试用环境
2026-01-20 11:21:40 +08:00
e36524de3a feat(credit): 添加用户真实姓名关联查询功能
- 在多个实体类中添加 realName 字段用于显示用户真实姓名
- 更新 XML 映射文件以关联查询 sys_user 表中的真实姓名
- 统一为所有信用相关实体添加真实姓名查询支持
- 修复了多个实体缺少 TableField 注解导入的问题
- 实现了跨库关联查询以获取用户的完整信息
2026-01-20 01:41:44 +08:00
e647a5d066 feat(import): 添加批量导入功能支持历史数据处理
- 新增 upsertBySingleKeyAndIncrementCounterOnUpdate 方法用于单字段键匹配的批量更新插入操作
- 新增 upsertByCodeOrNameAndIncrementCounterOnUpdate 方法用于代码或名称匹配的批量更新插入操作
- 在 CreditAdministrativeLicenseController 中添加历史行政许可批量导入接口
- 在 CreditBankruptcyController 中添加历史破产重整批量导入接口
- 在 CreditBreachOfTrustController 中添加历史失信被执行人批量导入接口
- 在 CreditCourtSessionController 中添加历史开庭公告批量导入接口
- 实现基于案号或名称的重复数据检测和计数器递增逻辑
- 添加 Excel 文件解析和超链接读取功能支持
- 实现分块处理机制提高大批量数据导入性能
- 添加异常处理和错误消息收集机制确保导入过程稳定性
2026-01-20 00:53:58 +08:00
fc0dc99ccc feat(credit): 添加历史被执行人批量导入功能
- 新增 /import/history 接口支持历史被执行人数据批量导入
- 实现 Excel 和 ZIP 格式文件的历史被执行人数据解析
- 添加案号重复时取最后一条记录的去重逻辑
- 支持 ZIP 文件自动解压并逐个处理内部 Excel 文件
- 实现导入过程中的数据验证和错误信息收集
- 添加 hyperlink 链接提取功能用于案号和被执行人名称
- 实现按案号 upsert 更新或插入历史被执行人记录
- 支持导入时设置企业 ID、用户 ID 和租户 ID 等上下文信息
- 提供详细的导入结果统计包括成功和失败数量
- 删除不再使用的 CreditJudgmentDebtorHistory 相关代码文件
2026-01-20 00:24:32 +08:00
850c18d639 fix(import): 修复导入参数中的Excel注解配置
- 将dataType字段的@Schema注解替换为@Excel注解
- 将plaintiffAppellant字段的@Schema注解替换为@Excel注解
- 确保字段正确映射到Excel导入功能
2026-01-19 22:44:38 +08:00
c902bbd214 feat: 优化失信记录实体和导入功能
- 调整CreditBreachOfTrust实体中url字段的位置
- 为法院公告导入增加多sheet文件兼容性支持
- 为开庭公告导入增加多sheet文件兼容性支持
- 导入时优先查找指定名称的sheet,如"法院公告"或"开庭公告"
- 当指定sheet不存在时,默认使用第0个sheet进行导入
2026-01-19 21:56:22 +08:00
84e6222c4d refactor(credit): 重构竞争对手实体的公司名称字段
- 将 CreditCompetitor 实体中的 companyName 字段重命名为 name
- 保留 companyName 字段作为关联查询的临时字段(exist=false)
- 更新控制器中导入逻辑,使用 name 字段进行数据处理
- 修改 XML 映射文件中的查询条件,使用 name 替代 companyName
- 更新导入参数和查询参数类中的字段映射
- 修复专利控制器中的导入验证逻辑
- 调整外部投资企业实体的字段描述文案
2026-01-19 21:20:12 +08:00
ba683bd578 refactor(credit): 重构竞争对手实体的公司名称字段
- 将 CreditCompetitor 实体中的 companyName 字段重命名为 name
- 保留 companyName 字段作为关联查询的临时字段(exist=false)
- 更新控制器中导入逻辑,使用 name 字段进行数据处理
- 修改 XML 映射文件中的查询条件,使用 name 替代 companyName
- 更新导入参数和查询参数类中的字段映射
- 修复专利控制器中的导入验证逻辑
- 调整外部投资企业实体的字段描述文案
2026-01-19 17:26:28 +08:00
12fc77b35c feat(import): 实现Excel导入时链接地址自动提取功能
- 修改CreditAdministrativeLicense相关类将"许可类型"字段描述改为"许可类别"
- 在多个实体类(CreditBreachOfTrust、CreditCompetitor、CreditCourtAnnouncement等)中新增url字段用于存储链接地址
- 重构CreditCompany实体类调整字段顺序和位置
- 在各个控制器中实现Excel导入时链接地址的自动提取和填充逻辑
- 新增readKeyValueByHeaders方法支持从指定列读取键值对数据
- 新增readUrlByKey方法支持从超链接或独立列提取URL地址
- 优化Excel导入流程增加链接地址批量处理功能
- 修复EasyPOI无法读取单元格超链接地址的问题
2026-01-19 14:23:48 +08:00
071c44679a refactor(credit): 重构司法信用实体和导入参数结构
- 修改 CreditBreachOfTrust 实体字段定义,调整案号、当事人、涉案金额等字段映射
- 创建新的 CreditBreachOfTrustImportParam 导入参数类替代原有司法通用参数
- 更新 Controller 中的导入功能实现,使用新的参数类进行数据转换
- 调整查询条件过滤逻辑,移除不必要的字段匹配
- 为终本案件模块创建独立的导入参数类 CreditFinalVersionImportParam
- 优化导入模板生成逻辑,支持按标签页名称查找对应工作表
- 重构终本案件实体字段映射,调整被执行人和申请执行人字段定义
- 更新 Excel 导入验证逻辑,简化空行判断条件
2026-01-19 13:33:29 +08:00
f2f7595674 refactor(credit): 重构司法信用实体和导入参数结构
- 修改 CreditBreachOfTrust 实体字段定义,调整案号、当事人、涉案金额等字段映射
- 创建新的 CreditBreachOfTrustImportParam 导入参数类替代原有司法通用参数
- 更新 Controller 中的导入功能实现,使用新的参数类进行数据转换
- 调整查询条件过滤逻辑,移除不必要的字段匹配
- 为终本案件模块创建独立的导入参数类 CreditFinalVersionImportParam
- 优化导入模板生成逻辑,支持按标签页名称查找对应工作表
- 重构终本案件实体字段映射,调整被执行人和申请执行人字段定义
- 更新 Excel 导入验证逻辑,简化空行判断条件
2026-01-19 12:02:53 +08:00
f799c2d7ea refactor(credit): 重构司法信用实体和导入参数结构
- 修改 CreditBreachOfTrust 实体字段定义,调整案号、当事人、涉案金额等字段映射
- 创建新的 CreditBreachOfTrustImportParam 导入参数类替代原有司法通用参数
- 更新 Controller 中的导入功能实现,使用新的参数类进行数据转换
- 调整查询条件过滤逻辑,移除不必要的字段匹配
- 为终本案件模块创建独立的导入参数类 CreditFinalVersionImportParam
- 优化导入模板生成逻辑,支持按标签页名称查找对应工作表
- 重构终本案件实体字段映射,调整被执行人和申请执行人字段定义
- 更新 Excel 导入验证逻辑,简化空行判断条件
2026-01-19 11:59:44 +08:00
18f2903f35 refactor(credit): 重构司法信用实体和导入参数结构
- 修改 CreditBreachOfTrust 实体字段定义,调整案号、当事人、涉案金额等字段映射
- 创建新的 CreditBreachOfTrustImportParam 导入参数类替代原有司法通用参数
- 更新 Controller 中的导入功能实现,使用新的参数类进行数据转换
- 调整查询条件过滤逻辑,移除不必要的字段匹配
- 为终本案件模块创建独立的导入参数类 CreditFinalVersionImportParam
- 优化导入模板生成逻辑,支持按标签页名称查找对应工作表
- 重构终本案件实体字段映射,调整被执行人和申请执行人字段定义
- 更新 Excel 导入验证逻辑,简化空行判断条件
2026-01-19 00:05:11 +08:00
d7c15cb22f refactor(credit): 重构司法信用实体和导入参数结构
- 修改 CreditBreachOfTrust 实体字段定义,调整案号、当事人、涉案金额等字段映射
- 创建新的 CreditBreachOfTrustImportParam 导入参数类替代原有司法通用参数
- 更新 Controller 中的导入功能实现,使用新的参数类进行数据转换
- 调整查询条件过滤逻辑,移除不必要的字段匹配
- 为终本案件模块创建独立的导入参数类 CreditFinalVersionImportParam
- 优化导入模板生成逻辑,支持按标签页名称查找对应工作表
- 重构终本案件实体字段映射,调整被执行人和申请执行人字段定义
- 更新 Excel 导入验证逻辑,简化空行判断条件
2026-01-18 23:50:07 +08:00
3582a3076e refactor(credit): 调整司法文书实体和参数结构
- 移除 dataType、plaintiffAppellant 等字段,新增 title 字段
- 重命名 involvedAmount 为案件金额(元),defendantAppellee 为裁判结果
- 新增 releaseDate 发布日期字段和 defendantAppellee 裁判结果字段
- 更新导入参数类 CreditJudicialDocumentImportParam 替代旧参数类
- 修改
2026-01-18 23:01:39 +08:00
e4a3ea9c7f refactor(credit): 调整司法文书实体和参数结构
- 移除 dataType、plaintiffAppellant 等字段,新增 title 字段
- 重命名 involvedAmount 为案件金额(元),defendantAppellee 为裁判结果
- 新增 releaseDate 发布日期字段和 defendantAppellee 裁判结果字段
- 更新导入参数类 CreditJudicialDocumentImportParam 替代旧参数类
- 修改
2026-01-18 22:16:40 +08:00
e62b900bb1 refactor(judicial): 优化法院公告和送达公告的数据结构和导入功能
- 重命名 CreditJudicialImportParam 为 CreditCourtAnnouncementImportParam 和 CreditDeliveryNoticeImportParam
- 更新实体类字段描述,将 dataType 改为 公告类型,plaintiffAppellant 改为 公告人,occurrenceTime 改为 刊登日期
- 移除不再使用的字段如 appellee、involvedAmount、courtName、dataStatus 等
- 添加送达公告的 url 字段用于存储链接地址
- 更新导入模板生成逻辑,适配新的参数类结构
- 优化导入验证方法,移除对已删除字段的检查
- 在送达公告导入中增加超链接提取功能,从案号列读取URL信息
- 更新数据库查询条件,移除对已删除字段的过滤条件
2026-01-18 11:44:46 +08:00
4794a9af3e feat(judicial): 增加URL字段并优化Excel导入功能
- 在CreditCaseFiling、CreditCourtSession和CreditMediation实体中新增URL字段
- 实现Excel导入时自动读取案号列的超链接地址并回填到URL字段
- 创建独立的CreditCourtSessionImportParam和CreditMediationImportParam导入参数类
- 将立案时间字段名称统一调整为立案日期
- 优化导入模板生成和空行判断逻辑
- 更新导入参数转换方法,移除冗余字段映射
2026-01-18 11:16:35 +08:00
07ea48795b feat(credit): 更新立案信息导入功能
- 修改CreditCaseFiling实体类中occurrenceTime字段描述为"立案时间"
- 替换导入参数类从CreditJudicialImportParam为CreditCaseFilingImportParam
- 更新导入接口文档注释从"司法大数据"为"立案信息"
- 实现Excel导入时指定"立案信息"工作表索引进行读取
- 更新模板下载功能使用新的导入参数类并修改模板名称
- 新增CreditCaseFilingImportParam导入参数类定义立案信息字段
- 在CreditExternal实体类中新增url字段用于存储链接地址
2026-01-18 10:02:30 +08:00
d49ce6e73e feat(excel): 优化Excel导入功能支持指定工作表名称
- 在CreditBreachOfTrustController中添加对"失信被执行人"工作表的精确匹配
- 在CreditXgxfController中添加对"限制高消费"工作表的精确匹配
- 在CreditJudgmentDebtorController中实现被执行人工作表的优先级排序逻辑
- 新增normalizeSheetName方法统一处理工作表名称的空格和特殊字符
- 改进findDebtorSheetIndices方法优先选择名为"被执行人"的工作表
- 调整ExcelImportSupport调用方式以支持指定工作表索引参数
2026-01-16 15:31:27 +08:00
e6736d41ff refactor(import): 优化批量导入功能提升性能
- 在多个控制器中引入 BatchImportSupport 工具类
- 实现分块处理机制,每批次处理 500 条记录
- 使用 persistChunkWithFallback 方法替代逐条保存
- 保持原有的数据校验和去重逻辑不变
- 显著提升大量数据导入时的执行效率
- 减少数据库操作次数降低系统负载
2026-01-16 00:55:49 +08:00
e0e15cdd45 feat(mapper): 添加公司关联查询和关键词搜索功能
- 在多个Mapper XML文件中添加LEFT JOIN credit_company表关联
- 扩展关键词搜索范围,支持通过公司名称进行搜索匹配
- 更新CreditNearbyCompany相关功能,支持按公司ID筛选和导入
- 修改CreditNearbyCompanyParam中companyId字段类型为String
- 暂时注释掉纳税人识别号相关的搜索条件
- 统一各信用数据映射文件中的关键词搜索逻辑
2026-01-15 23:58:18 +08:00
2116856167 feat(mapper): 添加公司关联查询和关键词搜索功能
- 在多个Mapper XML文件中添加LEFT JOIN credit_company表关联
- 扩展关键词搜索范围,支持通过公司名称进行搜索匹配
- 更新CreditNearbyCompany相关功能,支持按公司ID筛选和导入
- 修改CreditNearbyCompanyParam中companyId字段类型为String
- 暂时注释掉纳税人识别号相关的搜索条件
- 统一各信用数据映射文件中的关键词搜索逻辑
2026-01-15 23:33:42 +08:00
e810136fbb feat(credit): 添加企业ID字段支持精确查询
- 在CreditNearbyCompany实体中新增companyId字段
- 更新CreditNearbyCompanyMapper.xml添加公司ID查询条件
- 在CreditNearbyCompanyParam参数类中添加companyId查询字段
- 实现基于企业ID的精确筛选功能
2026-01-15 22:42:07 +08:00
45828ad95c feat(car): 更新车辆查询支持组织ID列表筛选
- 添加 organizationIds 参数支持多组织ID筛选
- 实现 IN 查询逻辑替代原有的单个组织ID匹配
- 保留原有 organizationId 单值查询作为备选条件
- 添加 foreach 循环处理组织ID集合
- 优化查询条件避免参数冲突
2026-01-13 15:27:30 +08:00
f0cd200e21 feat(HjmCarParam): 添加组织ID集查询功能
- 引入java.util.Set导入支持
- 新增organizationIds字段用于组织ID集合查询
- 添加@Schema注解描述组织ID集功能
- 配置@QueryField注解支持IN查询类型
2026-01-13 15:22:24 +08:00
4d7f871aad feat(credit): 添加被执行人历史功能模块
- 在CreditJudgmentDebtor实体中增加historyId和historyName字段用于关联历史数据
- 新增CreditJudgmentDebtorHistory实体类用于存储被执行人历史记录
- 创建CreditJudgmentDebtorHistoryController提供完整的CRUD和批量导入导出接口
- 实现CreditJudgmentDebtorHistoryService及其实现类处理业务逻辑
- 添加CreditJudgmentDebtorHistoryMapper及XML映射文件支持数据库操作
- 新增CreditJudgmentDebtorHistoryParam和CreditJudgmentDebtorHistoryImportParam参数类
- 修改CreditJudgmentDebtorMapper.xml添加与历史表的关联查询
- 更新CreditUserMapper.xml在关键词搜索中加入历史被执行人名称匹配
2026-01-12 09:17:15 +08:00
c766f2879d feat(house): 添加租金和面积单位字段
- 为租金字段添加rentUnit单位属性
- 为月租金字段添加monthlyRentUnit单位属性
- 为面积字段添加extentUnit单位属性
- 更新实体类HouseInfo的数据结构定义
2026-01-09 19:08:48 +08:00
6eeadd7ec3 feat(credit): 支持指定工作表索引导入信用用户数据
- 添加 ExcelImportSupport.findSheetIndex 方法查找特定工作表
- 修改 tryImport 方法支持传入工作表索引参数
- 更新导入配置循环使用指定工作表索引
- 修改 readNameHyperlinks 方法使用正确的工作表索引读取超链接
- 设置 ImportParams 的起始工作表索引为动态获取的索引值
2026-01-09 10:13:56 +08:00
0de588321f feat(excel): 优化Excel导入功能支持动态工作表查找
- 添加findSheetIndex方法支持根据工作表名称查找索引
- 实现精确匹配、前缀匹配和包含匹配的查找逻辑
- 在各个控制器中使用动态工作表索引替换固定索引
- 优化CreditCompanyController中的必填字段验证逻辑
- 移除对code字段的必填验证要求
- 添加工作表名称标准化处理功能
2026-01-08 18:29:27 +08:00
3434f4d8e6 feat(excel): 优化Excel导入功能支持动态工作表查找
- 添加findSheetIndex方法支持根据工作表名称查找索引
- 实现精确匹配、前缀匹配和包含匹配的查找逻辑
- 在各个控制器中使用动态工作表索引替换固定索引
- 优化CreditCompanyController中的必填字段验证逻辑
- 移除对code字段的必填验证要求
- 添加工作表名称标准化处理功能
2026-01-08 18:16:44 +08:00
abbcd992a4 feat(credit): 添加批量导入功能和下载模板功能
- 为行政许可控制器添加批量导入和下载模板功能
- 为破产重整控制器添加批量导入和下载模板功能
- 为分支机构控制器添加批量导入和下载模板功能
- 为历史法定代表人控制器添加批量导入和下载模板功能
- 为附近企业控制器添加批量导入和下载模板功能
- 实现Excel导入数据验证和错误处理机制
- 添加导入参数实体类和转换方法
- 实现超链接读取和数据映射功能
- 添加导入模板生成和下载功能
2026-01-07 14:37:59 +08:00
947d1190a9 feat(credit): 添加行政许可、破产重整和分支机构管理功能
- 创建了CreditAdministrativeLicense实体类及对应的Controller、Service、Mapper和XML映射文件
- 创建了CreditBankruptcy实体类及对应的Controller、Service、Mapper和XML映射文件
- 创建了CreditBranch实体类及对应的Controller、Service、Mapper和XML映射文件
- 实现了分页查询、列表查询、详情查询、新增、修改、删除等基础CRUD功能
- 添加了批量操作功能包括批量新增、批量修改和批量删除
- 集成了权限控制和操作日志功能
- 实现了关联查询和排序功能
- 添加了完整的参数验证和查询条件支持
2026-01-07 13:59:30 +08:00
3ed457d7d6 feat(credit): 添加企业名称关联查询功能
- 在多个实体类中添加companyName字段用于显示企业名称
- 为CreditBreachOfTrust、CreditCaseFiling等实体添加TableField注解
- 修改CreditCompetitor实体的字段名称为mainCompanyName
- 在各个Mapper.xml文件中添加LEFT JOIN关联查询企业名称
- 更新查询SQL语句以包含企业名称字段
- 添加必要的TableField导入依赖
2026-01-05 15:21:59 +08:00
064d3b168e feat(import): 批量导入功能增加公司ID参数支持
- 为所有批量导入接口增加可选的companyId参数
- 在导入过程中将companyId设置到实体对象中
- 修复客户导入时的租户ID冲突处理逻辑
- 添加数据完整性验证异常处理机制
- 实现字符串字段的标准化处理功能
- 优化重复数据检测和更新逻辑
2026-01-05 14:30:46 +08:00
bb3cf97cc9 feat(credit): 添加企业ID字段支持
- 在多个信用实体类中添加companyId字段,包括CreditBreachOfTrust、CreditCaseFiling、CreditCompetitor等
- 更新对应的Mapper XML文件,添加基于companyId的查询条件
- 在各个参数类中添加companyId参数支持
- 为CreditJudicialImportParam添加缺失的Schema注解
- 实现基于企业ID的数据过滤功能,提升数据查询的准确性
2026-01-05 13:01:53 +08:00
2f39bd4e0b feat(import): 添加Excel导入时超链接解析功能
- 在CreditCompany和CreditJudgmentDebtor实体中添加url字段
- 实现ExcelImportSupport工具类的超链接读取功能
- 支持通过表头名称定位列并提取超链接地址
- 在企业信息导入时自动解析原文件和匹配名称的超链接
- 在执行人信息导入时解析案号和被执行人名称的超链接
- 移除多余的import语句和调试输出代码
- 扩展ImportResult类以支持工作表索引信息
2026-01-03 22:31:05 +08:00
ce01afcfb0 feat(controller): 增加被执行人批量导入支持ZIP和多sheet解析
- 支持ZIP文件批量导入多个Excel文件
- 增加对多sheet Excel文件的自动识别和解析
- 实现表头配置自动匹配和最优结果选择
- 添加导入结果统计和错误信息汇总
- 优化Excel导入的错误处理和字符编码支持
- 增加对被执行人工作表名称的智能识别
- 实现导入过程中的数据验证和重复检查机制
2026-01-03 22:01:35 +08:00
0dce41f2db fix(credit): 修复执行标的字段类型错误
- 将CreditJudgmentDebtor实体类中的amount字段类型从BigDecimal改为String
- 将CreditJudgmentDebtorParam参数类中的amount字段类型从BigDecimal改为String
- 在导入处理中移除对amount字段的BigDecimal解析
- 添加对name1字段的空值检查
- 调整债务人名称赋值逻辑,优先使用name字段,为空时使用name1字段
- 添加调试日志输出导入数据大小
2026-01-03 19:57:17 +08:00
4f1a931006 feat(credit): 扩展信用公司搜索功能并添加被执行人字段
- 修改CreditCompanyMapper.xml中的搜索条件,增加名称和匹配名称的模糊搜索
- 添加对信用代码的精确搜索功能
- 在CreditJudgmentDebtor实体中新增name1字段用于存储被执行人信息
- 为name1字段添加数据导入参数映射配置
- 更新数据库查询逻辑以支持多字段搜索匹配
2026-01-03 19:22:16 +08:00
0195e12409 led 2025-12-25 13:19:13 +08:00
3536028766 feat(credit): 添加股权冻结导入参数类
- 新增CreditGqdjImportParam类用于股权冻结数据导入
- 定义案号、冻结股权标的企业、被执行人等Excel字段映射
- 配置执行法院、执行标的金额等导入参数属性

refactor(led): 清理调试日志并优化令牌刷新

- 移除searchStopAndReplace方法中的调试打印语句
- 在令牌刷新成功后添加新的调试日志
- 优化令牌缓存和刷新流程的日志输出
2025-12-23 15:54:24 +08:00
c1d045b9b2 feat(credit): 导入功能支持读取项目名称超链接
- 在 CreditUser 实体类中新增 url 字段用于存储项目网址
- 在导入逻辑中增加读取 Excel 超链接的功能
- 新增 readNameHyperlinks 方法解析“项目名称”列的超链接
- 将读取到的超链接地址设置到对应 CreditUser 实体的 url 属性中
- 引入 Apache POI 相关依赖以支持 Excel 文件操作
- 更新导入处理流程,确保超链接与数据行正确匹配
2025-12-22 17:33:00 +08:00
0c6cb13ca7 feat(credit): 调整涉案金额和采购金额为字符串类型并优化Excel导入功能
- 将多个实体类中的involvedAmount字段由BigDecimal改为String类型
- 将CreditCustomer和CreditSupplier中的金额及日期字段调整为String类型
- 移除Excel导入时对金额字段的手动BigDecimal解析
- 增强ExcelImportSupport支持指定sheet索引读取功能
- 为CreditCustomer新增完整的Excel导入和模板下载接口
- 新增CreditCustomerImportParam用于导入数据映
2025-12-22 10:16:56 +08:00
42ebdf5653 feat(credit): 新增客户管理模块并优化字段类型
- 添加客户实体类 CreditCustomer 及相关控制器、服务、映射文件
- 统一将多个实体中的 LocalDate 和 BigDecimal 字段改为 String 类型
- 更新相关控制器逻辑,移除 ImportHelper 的日期和数值解析调用
- 修正 XML 映射文件中字段名引用错误的问题
- 调整部分表字段注释描述,如 defendant Appellee 改为 Appellee
2025-12-22 08:51:12 +08:00
cb8cc3c530 feat(credit): 添加失信被执行人和司法大数据管理功能
- 新增失信被执行人实体类、控制器、Mapper及Service实现
- 新增司法大数据实体类、控制器、Mapper及Service实现
- 实现分页查询、列表查询、详情查询接口
- 支持新增、修改、删除及批量操作接口
- 支持Excel模板下载与数据导入功能
- 配置MQTT生产环境启用开关及连接地址调整
- 移除旧审计报告相关控制器、枚举及DTO定义
2025-12-21 20:41:54 +08:00
6affaba5c3 feat(credit): 新增企业管理功能模块
- 新增企业实体类CreditCompany,包含企业基本信息字段
- 新增企业控制器CreditCompanyController,提供CRUD接口
- 新增企业导入参数类CreditCompanyImportParam,支持Excel导入
- 新增企业查询参数类CreditCompanyParam,支持条件查询
- 新增企业Mapper接口及XML映射文件,实现关联查询
- 实现企业分页查询、列表查询、详情查询接口
- 实现企业新增、修改、删除接口
- 实现企业批量导入功能,支持Excel模板下载
- 实现企业数据校验和重复数据处理逻辑
- 添加企业导入模板下载接口
- 支持企业信息的完整字段映射和转换逻辑
- 添加企业查询条件注解,支持动态SQL查询
- 实现企业关联查询SQL,支持多字段模糊匹配
- 添加企业操作日志记录和权限控制注解
- 完善企业导入异常处理和错误信息收集机制
2025-12-17 14:48:13 +08:00
57cdb72208 feat(credit): 新增企业管理功能模块
- 新增企业实体类CreditCompany,包含企业基本信息字段
- 新增企业控制器CreditCompanyController,提供CRUD接口
- 新增企业导入参数类CreditCompanyImportParam,支持Excel导入
- 新增企业查询参数类CreditCompanyParam,支持条件查询
- 新增企业Mapper接口及XML映射文件,实现关联查询
- 实
2025-12-17 10:04:00 +08:00
6321b69dc2 feat(credit): 新增司法案件管理模块
- 新增司法案件实体类CreditJudiciary,包含案件名称、案号、类型等字段
- 新增司法案件控制器CreditJudiciaryController,提供CRUD及批量操作接口
- 新增司法案件Mapper及XML文件,支持分页和列表查询
- 新增司法案件Service及实现类,封装业务逻辑
- 新增司法案件导入参数类CreditJudiciaryImportParam,支持Excel导入功能
- 新增司法案件查询参数类CreditJudiciaryParam,支持条件查询
- 修改CreditUser相关类注释,将“赊账客户”改为“招投标信息”
- 优化导入功能提示文案,区分新增与更新数量统计
2025-12-16 16:28:47 +08:00
14d1c83139 feat(credit): 新增客户发布日期字段
- 在示例数据中添加 releaseDate 字段
- 在实体映射中增加 releaseDate 属性同步逻辑
2025-12-16 15:01:40 +08:00
522 changed files with 50121 additions and 2128 deletions

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ ehthumbs.db
Thumbs.db Thumbs.db
/file/ /file/
/websoft-modules.log /websoft-modules.log
/tmp/

View File

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

View File

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

View File

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

View File

@@ -31,7 +31,7 @@
#### 外部服务 #### 外部服务
- **MQTT服务**: 连接成功 ✅ - **MQTT服务**: 连接成功 ✅
- 服务器: tcp://1.14.159.185:1883 - 服务器: tcp://132.232.214.96:1883
- 客户端ID: hjm_car_1753549632706 - 客户端ID: hjm_car_1753549632706
- 主题订阅: /SW_GPS/# - 主题订阅: /SW_GPS/#
- **Redis**: 连接配置正常 ✅ - **Redis**: 连接配置正常 ✅

View File

@@ -0,0 +1,26 @@
# ShopDealerReferee 绑定接口规则与索引建议
接口:`POST /api/shop/shop-dealer-referee`
## 业务规则(后端)
- 邀请人(dealerId)必须存在且有效:以 `shop_dealer_user` 记录存在且 `is_delete=0` 为准
- 当前用户(userId)仅从 token 获取;若 body.userId 存在且与 token 不一致则拒绝
- 禁止自己绑定自己:`dealerId == userId`
- 仅首次绑定生效:若已存在(同一 tenant、同一 user、`level=1`)则直接返回成功(幂等,不改绑)
- 记录溯源字段:`source``scene`
## 并发幂等(数据库建议)
建议在 `shop_dealer_referee` 增加溯源字段,并加唯一索引保证并发下不重复写入:
```sql
ALTER TABLE shop_dealer_referee
ADD COLUMN source VARCHAR(32) NULL COMMENT '来源(如 goods_share)',
ADD COLUMN scene VARCHAR(255) NULL COMMENT '场景参数(溯源统计)';
-- 约束:同一 tenant 下,一个用户每个 level 只能有一条推荐关系
ALTER TABLE shop_dealer_referee
ADD UNIQUE KEY uk_shop_dealer_referee_tenant_user_level (tenant_id, user_id, level);
```

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
package com.gxwebsoft.ai.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.knowledge-base")
public class KnowledgeBaseConfig {
private String accessKeyId;
private String accessKeySecret;
private String workspaceId;
}

View File

@@ -1,12 +0,0 @@
package com.gxwebsoft.ai.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "ai.template")
public class TemplateConfig {
private String wordTemplatePath;
}

View File

@@ -1,19 +0,0 @@
package com.gxwebsoft.ai.constants;
public class KnowledgeBaseConstants {
public static final String[] KEY_WORDS = {
"",
"审计依据 法律法规 审计业务约定书 经济责任审计管理办法 中共中央办公厅 国务院办公厅 党政主要领导干部审计规定 国家法规 公司管理制度 年度工作目标 党政主要领导干部经济责任审计规定",
"审计目标 经济责任审计目标 资产负债损益真实性 合法性 效益性 经济指标完成情况 重大决策执行 遵守财经法规 国有资产保值增值 经济责任评价 任职期间履职评价 责任界定 业绩评价",
"审计对象 审计范围 被审计领导干部 [职务] [姓名] 任职期间 [开始日期]至[结束日期] 职务任期 重大问题追溯 重要事项延伸 审计时限 下属子公司 代管企业",
"被审计单位基本情况 单位概况 组织机构 人员结构 财务会计政策 合并口径财务数据 资产总额 负债总额 营业收入 利润 内部控制制度 子公司 代管企业 职能部门设置 合并财务报表",
"审计内容 审计重点 贯彻执行经济方针 重大决策执行 发展战略 年度目标完成 法人治理结构 内部控制 财务真实性 风险管控 党风廉政建设 以往审计整改 三重一大经济决策 资产管理 采购管理 债权债务",
"审计风险 证据不充分 评价不客观 内部控制失效 法律法规变化 风险应对策略 审计证据充分性 评价客观性 内部控制审查 法规政策跟踪 重要性水平",
"审计方法 穿行测试 趋势分析 比率分析 访谈法 数据分析 分析性程序 检查 监盘 观察 询问 函证 计算 重新执行",
"审计步骤 时间安排 准备阶段 实施阶段 报告阶段 归档阶段 审计人员安排 资料收集 实质性程序 审计报告编写 交换意见 审计归档 进点会 进度表",
"审计组织实施 审计组人员分工 职责分配 审计工作计划 前期调研 审前培训 实地审计 质量控制 内部培训 沟通协调 分级复核 集体讨论 重大事项汇报 里程碑事件清单 审计工作组 项目负责人 主审"
};
}

View File

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

View File

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

View File

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

View File

@@ -1,144 +0,0 @@
package com.gxwebsoft.ai.controller;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.JSONObject;
import com.gxwebsoft.ai.config.TemplateConfig;
import com.gxwebsoft.ai.dto.AuditReportRequest;
import com.gxwebsoft.ai.dto.KnowledgeBaseRequest;
import com.gxwebsoft.ai.enums.AuditReportEnum;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.entity.User;
import cn.afterturn.easypoi.word.WordExportUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
* 审计报告控制器
* @author GIIT-YC
*
*/
@Tag(name = "审计报告")
@RestController
@RequestMapping("/api/ai/auditReport")
public class AuditReportController extends BaseController {
@Autowired
private TemplateConfig templateConfig;
@Autowired
private KnowledgeBaseController knowledgeBaseController;
private String invok(String query, String knowledge, String history, String suggestion, String userName) {
// 构建请求体
JSONObject requestBody = new JSONObject();
JSONObject inputs = new JSONObject();
inputs.put("query", query);
inputs.put("knowledge", knowledge);
inputs.put("history", history);
inputs.put("suggestion", suggestion);
requestBody.put("inputs", inputs);
requestBody.put("response_mode", "blocking");
requestBody.put("user", userName);
// 发送 POST 请求
String result = HttpUtil.createPost("http://1.14.159.185:8180/v1/workflows/run")
.header("Authorization", "Bearer app-d7Ok9FECVZG2Ybw9wpg7tGu9")
.header("Content-Type", "application/json")
.body(requestBody.toString())
.timeout(600000)
.execute()
.body();
// 解析返回的JSON字符串
JSONObject jsonResponse = JSONObject.parseObject(result);
// 获取data字段
JSONObject data = jsonResponse.getJSONObject("data");
// 获取outputs字段
JSONObject outputs = data.getJSONObject("outputs");
// 获取outputs中的result字符串
String resultStr = outputs.getString("result");
return resultStr;
}
/**
* 生成审计报告-单一模块
*/
@Operation(summary = "生成审计报告-单一模块")
@PostMapping("/generate")
public ApiResult<?> generateAuditReport(@RequestBody AuditReportRequest req) {
final User loginUser = getLoginUser();
KnowledgeBaseRequest knowledgeBaseRequest = new KnowledgeBaseRequest();
knowledgeBaseRequest.setKbId(req.getKbId());
knowledgeBaseRequest.setFormCommit((req.getFormCommit() > 10) ? req.getFormCommit() / 10 : req.getFormCommit());
String knowledge = knowledgeBaseController.query(knowledgeBaseRequest).getData().toString();
String query = AuditReportEnum.getByCode(req.getFormCommit()).getDesc();
String ret = this.invok(query, knowledge, req.getHistory(), req.getSuggestion(), loginUser.getUsername());
return success(ret);
}
/**
* 生成并下载审计报告
*/
@Operation(summary = "生成并下载审计报告")
@PostMapping("/download")
public void downloadAuditReport(@RequestBody AuditReportRequest req, HttpServletResponse response) {
// 保存原始的安全阈值
double originalMinInflateRatio = ZipSecureFile.getMinInflateRatio();
try {
// 降低Zip bomb检测的阈值解决模板文件的安全检测问题
ZipSecureFile.setMinInflateRatio(0.001);
// 准备模板数据
Map<String, Object> map = new HashMap<>();
map.put(AuditReportEnum.AUDIT_TITLE.getCode().toString(), req.getFrom0());
map.put(AuditReportEnum.AUDIT_BASIS.getCode().toString(), req.getFrom1());
map.put(AuditReportEnum.AUDIT_OBJECTIVE.getCode().toString(), req.getFrom2());
map.put(AuditReportEnum.AUDIT_SCOPE.getCode().toString(), req.getFrom3());
map.put(AuditReportEnum.UNIT_OVERVIEW.getCode().toString(), req.getFrom41());
map.put(AuditReportEnum.ORG_AND_PERSONNEL.getCode().toString(), req.getFrom42());
map.put(AuditReportEnum.AUDIT_CONTENT_METHODS.getCode().toString(), req.getFrom5());
// 使用Easypoi的Word模板功能
XWPFDocument document = WordExportUtil.exportWord07(templateConfig.getWordTemplatePath(), map);
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Content-Disposition", "attachment; filename=audit_report.docx");
// 输出到响应流
OutputStream out = response.getOutputStream();
document.write(out);
out.flush();
out.close();
} catch (Exception e) {
throw new RuntimeException("生成审计报告失败", e);
} finally {
// 恢复原始的安全阈值
ZipSecureFile.setMinInflateRatio(originalMinInflateRatio);
}
}
}

View File

@@ -1,59 +0,0 @@
package com.gxwebsoft.ai.controller;
import com.aliyun.bailian20231229.Client;
import com.aliyun.bailian20231229.models.RetrieveResponse;
import com.aliyun.bailian20231229.models.RetrieveResponseBody.RetrieveResponseBodyDataNodes;
import com.gxwebsoft.ai.config.KnowledgeBaseConfig;
import com.gxwebsoft.ai.constants.KnowledgeBaseConstants;
import com.gxwebsoft.ai.factory.KnowledgeBaseClientFactory;
import com.gxwebsoft.ai.util.KnowledgeBaseRetrieve;
import com.gxwebsoft.ai.dto.KnowledgeBaseRequest;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import cn.hutool.core.util.StrUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@Tag(name = "知识库")
@RestController
@RequestMapping("/api/ai/knowledgeBase")
public class KnowledgeBaseController extends BaseController {
@Autowired
private KnowledgeBaseConfig config;
@Autowired
private KnowledgeBaseClientFactory clientFactory;
@Operation(summary = "查询知识库")
@GetMapping("/query")
public ApiResult<?> query(KnowledgeBaseRequest req) {
Set<String> ret = new LinkedHashSet<>();
String workspaceId = config.getWorkspaceId();
List<String> keyWords = Arrays.asList(KnowledgeBaseConstants.KEY_WORDS);
String indexId = req.getKbId();
String query = StrUtil.isEmpty(req.getQuery()) ? keyWords.get(req.getFormCommit()) : req.getQuery();
Integer topK = req.getTopK() == null ? 10 : req.getTopK();
try {
Client client = clientFactory.createClient();
RetrieveResponse resp = KnowledgeBaseRetrieve.retrieveIndex(client, workspaceId, indexId, query);
for (RetrieveResponseBodyDataNodes node : resp.getBody().getData().getNodes()) {
ret.add(node.getText());
if (ret.size() >= topK) {
break;
}
}
} catch (Exception e) {
return fail("查询失败:" + e.getMessage());
}
return success(ret);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,42 +0,0 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class AuditReportRequest{
@Schema(description = "审计标题")
private String from0;
@Schema(description = "审计依据")
private String from1;
@Schema(description = "审计目标")
private String from2;
@Schema(description = "审计对象和范围")
private String from3;
@Schema(description = "被审计单位基本情况-单位概况")
private String from41;
@Schema(description = "被审计单位基本情况-机构和人员相关情况")
private String from42;
@Schema(description = "审计内容和重点及审计方法")
private String from5;
@Schema(description = "知识库ID")
private String kbId;
@Schema(description = "生成模块:AuditReportEnum.code")
private Integer formCommit;
@Schema(description = "历史内容")
private String history;
@Schema(description = "修改建议")
private String suggestion;
}

View File

@@ -1,20 +0,0 @@
package com.gxwebsoft.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class KnowledgeBaseRequest {
@Schema(description = "知识库ID")
private String kbId;
@Schema(description = "召回内容")
private String query;
@Schema(description = "召回模块1~9")
private Integer formCommit;
@Schema(description = "返回TOP切片数量")
private Integer topK;
}

View File

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

View File

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

View File

@@ -1,45 +0,0 @@
package com.gxwebsoft.ai.enums;
public enum AuditReportEnum {
AUDIT_TITLE(0, "审计标题"),
AUDIT_BASIS(1, "审计依据"),
AUDIT_OBJECTIVE(2, "审计目标"),
AUDIT_SCOPE(3, "审计对象和范围"),
UNIT_OVERVIEW(41, "被审计单位基本情况-单位概况"),
ORG_AND_PERSONNEL(42, "被审计单位基本情况-机构和人员相关情况"),
AUDIT_CONTENT_METHODS(5, "审计内容和重点及审计方法");
private final Integer code;
private final String desc;
AuditReportEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
public static AuditReportEnum getByCode(Integer code) {
for (AuditReportEnum value : values()) {
if (value.code.equals(code)) {
return value;
}
}
return null;
}
/**
* 根据代码获取描述信息
*/
public static String getDescByCode(Integer code) {
AuditReportEnum enumValue = getByCode(code);
return enumValue != null ? enumValue.getDesc() : null;
}
}

View File

@@ -1,23 +0,0 @@
package com.gxwebsoft.ai.factory;
import com.aliyun.bailian20231229.Client;
import com.aliyun.teaopenapi.models.Config;
import com.gxwebsoft.ai.config.KnowledgeBaseConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class KnowledgeBaseClientFactory {
@Autowired
private KnowledgeBaseConfig config;
public Client createClient() throws Exception {
Config authConfig = new Config()
.setAccessKeyId(config.getAccessKeyId())
.setAccessKeySecret(config.getAccessKeySecret());
// 下方接入地址以公有云的公网接入地址为例,可按需更换接入地址。
authConfig.endpoint = "bailian.cn-beijing.aliyuncs.com";
return new com.aliyun.bailian20231229.Client(authConfig);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,384 +0,0 @@
package com.gxwebsoft.ai.util;
import com.aliyun.bailian20231229.models.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.FileInputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 创建知识库
* @author GIIT-YC
*
*/
public class KnowledgeBaseCreate {
String ALIBABA_CLOUD_ACCESS_KEY_ID = "LTAI5tD5YRKuxWz6Eg7qrM4P";
String ALIBABA_CLOUD_ACCESS_KEY_SECRET = "bO8TBDXflOwbtSKimPpG8XrJnyzgTk";
String WORKSPACE_ID = "llm-4pf5auwewoz34zqu";
/**
* 检查并提示设置必要的环境变量。
*
* @return true 如果所有必需的环境变量都已设置,否则 false
*/
public static boolean checkEnvironmentVariables() {
Map<String, String> requiredVars = new HashMap<>();
requiredVars.put("ALIBABA_CLOUD_ACCESS_KEY_ID", "阿里云访问密钥ID");
requiredVars.put("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "阿里云访问密钥密码");
requiredVars.put("WORKSPACE_ID", "阿里云百炼业务空间ID");
List<String> missingVars = new ArrayList<>();
for (Map.Entry<String, String> entry : requiredVars.entrySet()) {
String value = System.getenv(entry.getKey());
if (value == null || value.isEmpty()) {
missingVars.add(entry.getKey());
System.out.println("错误:请设置 " + entry.getKey() + " 环境变量 (" + entry.getValue() + ")");
}
}
return missingVars.isEmpty();
}
/**
* 计算文档的MD5值。
*
* @param filePath 文档本地路径
* @return 文档的MD5值
* @throws Exception 如果计算过程中发生错误
*/
public static String calculateMD5(String filePath) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
try (FileInputStream fis = new FileInputStream(filePath)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
md.update(buffer, 0, bytesRead);
}
}
StringBuilder sb = new StringBuilder();
for (byte b : md.digest()) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
/**
* 获取文档大小(以字节为单位)。
*
* @param filePath 文档本地路径
* @return 文档大小(以字节为单位)
*/
public static String getFileSize(String filePath) {
File file = new File(filePath);
long fileSize = file.length();
return String.valueOf(fileSize);
}
/**
* 初始化客户端Client
*
* @return 配置好的客户端对象
*/
public static com.aliyun.bailian20231229.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
.setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));
// 下方接入地址以公有云的公网接入地址为例,可按需更换接入地址。
config.endpoint = "bailian.cn-beijing.aliyuncs.com";
return new com.aliyun.bailian20231229.Client(config);
}
/**
* 申请文档上传租约。
*
* @param client 客户端对象
* @param categoryId 类目ID
* @param fileName 文档名称
* @param fileMd5 文档的MD5值
* @param fileSize 文档大小(以字节为单位)
* @param workspaceId 业务空间ID
* @return 阿里云百炼服务的响应对象
*/
public static ApplyFileUploadLeaseResponse applyLease(com.aliyun.bailian20231229.Client client, String categoryId,
String fileName, String fileMd5, String fileSize, String workspaceId) throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.bailian20231229.models.ApplyFileUploadLeaseRequest applyFileUploadLeaseRequest = new com.aliyun.bailian20231229.models.ApplyFileUploadLeaseRequest();
applyFileUploadLeaseRequest.setFileName(fileName);
applyFileUploadLeaseRequest.setMd5(fileMd5);
applyFileUploadLeaseRequest.setSizeInBytes(fileSize);
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
ApplyFileUploadLeaseResponse applyFileUploadLeaseResponse = null;
applyFileUploadLeaseResponse = client.applyFileUploadLeaseWithOptions(categoryId, workspaceId,
applyFileUploadLeaseRequest, headers, runtime);
return applyFileUploadLeaseResponse;
}
/**
* 上传文档到临时存储。
*
* @param preSignedUrl 上传租约中的 URL
* @param headers 上传请求的头部
* @param filePath 文档本地路径
* @throws Exception 如果上传过程中发生错误
*/
public static void uploadFile(String preSignedUrl, Map<String, String> headers, String filePath) throws Exception {
File file = new File(filePath);
if (!file.exists() || !file.isFile()) {
throw new IllegalArgumentException("文件不存在或不是普通文件: " + filePath);
}
try (FileInputStream fis = new FileInputStream(file)) {
URL url = new URL(preSignedUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("PUT");
conn.setDoOutput(true);
// 设置上传请求头
conn.setRequestProperty("X-bailian-extra", headers.get("X-bailian-extra"));
conn.setRequestProperty("Content-Type", headers.get("Content-Type"));
// 分块读取并上传文档
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
conn.getOutputStream().write(buffer, 0, bytesRead);
}
int responseCode = conn.getResponseCode();
if (responseCode != 200) {
throw new RuntimeException("上传失败: " + responseCode);
}
}
}
/**
* 将文档添加到类目中。
*
* @param client 客户端对象
* @param leaseId 租约ID
* @param parser 用于文档的解析器
* @param categoryId 类目ID
* @param workspaceId 业务空间ID
* @return 阿里云百炼服务的响应对象
*/
public static AddFileResponse addFile(com.aliyun.bailian20231229.Client client, String leaseId, String parser,
String categoryId, String workspaceId) throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.bailian20231229.models.AddFileRequest addFileRequest = new com.aliyun.bailian20231229.models.AddFileRequest();
addFileRequest.setLeaseId(leaseId);
addFileRequest.setParser(parser);
addFileRequest.setCategoryId(categoryId);
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
return client.addFileWithOptions(workspaceId, addFileRequest, headers, runtime);
}
/**
* 查询文档的基本信息。
*
* @param client 客户端对象
* @param workspaceId 业务空间ID
* @param fileId 文档ID
* @return 阿里云百炼服务的响应对象
*/
public static DescribeFileResponse describeFile(com.aliyun.bailian20231229.Client client, String workspaceId,
String fileId) throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
return client.describeFileWithOptions(workspaceId, fileId, headers, runtime);
}
/**
* 在阿里云百炼服务中创建知识库(初始化)。
*
* @param client 客户端对象
* @param workspaceId 业务空间ID
* @param fileId 文档ID
* @param name 知识库名称
* @param structureType 知识库的数据类型
* @param sourceType 应用数据的数据类型,支持类目类型和文档类型
* @param sinkType 知识库的向量存储类型
* @return 阿里云百炼服务的响应对象
*/
public static CreateIndexResponse createIndex(com.aliyun.bailian20231229.Client client, String workspaceId,
String fileId, String name, String structureType, String sourceType, String sinkType) throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.bailian20231229.models.CreateIndexRequest createIndexRequest = new com.aliyun.bailian20231229.models.CreateIndexRequest();
createIndexRequest.setStructureType(structureType);
createIndexRequest.setName(name);
createIndexRequest.setSourceType(sourceType);
createIndexRequest.setSinkType(sinkType);
createIndexRequest.setDocumentIds(Collections.singletonList(fileId));
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
return client.createIndexWithOptions(workspaceId, createIndexRequest, headers, runtime);
}
/**
* 向阿里云百炼服务提交索引任务。
*
* @param client 客户端对象
* @param workspaceId 业务空间ID
* @param indexId 知识库ID
* @return 阿里云百炼服务的响应对象
*/
public static SubmitIndexJobResponse submitIndex(com.aliyun.bailian20231229.Client client, String workspaceId,
String indexId) throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.bailian20231229.models.SubmitIndexJobRequest submitIndexJobRequest = new com.aliyun.bailian20231229.models.SubmitIndexJobRequest();
submitIndexJobRequest.setIndexId(indexId);
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
return client.submitIndexJobWithOptions(workspaceId, submitIndexJobRequest, headers, runtime);
}
/**
* 查询索引任务状态。
*
* @param client 客户端对象
* @param workspaceId 业务空间ID
* @param jobId 任务ID
* @param indexId 知识库ID
* @return 阿里云百炼服务的响应对象
*/
public static GetIndexJobStatusResponse getIndexJobStatus(com.aliyun.bailian20231229.Client client,
String workspaceId, String jobId, String indexId) throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.bailian20231229.models.GetIndexJobStatusRequest getIndexJobStatusRequest = new com.aliyun.bailian20231229.models.GetIndexJobStatusRequest();
getIndexJobStatusRequest.setIndexId(indexId);
getIndexJobStatusRequest.setJobId(jobId);
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
GetIndexJobStatusResponse getIndexJobStatusResponse = null;
getIndexJobStatusResponse = client.getIndexJobStatusWithOptions(workspaceId, getIndexJobStatusRequest, headers,
runtime);
return getIndexJobStatusResponse;
}
/**
* 使用阿里云百炼服务创建知识库。
*
* @param filePath 文档本地路径
* @param workspaceId 业务空间ID
* @param name 知识库名称
* @return 如果成功返回知识库ID否则返回 null
*/
public static String createKnowledgeBase(String filePath, String workspaceId, String name) {
// 设置默认值
String categoryId = "default";
String parser = "DASHSCOPE_DOCMIND";
String sourceType = "DATA_CENTER_FILE";
String structureType = "unstructured";
String sinkType = "DEFAULT";
try {
// 步骤1初始化客户端Client
System.out.println("步骤1初始化Client");
com.aliyun.bailian20231229.Client client = createClient();
// 步骤2准备文档信息
System.out.println("步骤2准备文档信息");
String fileName = new File(filePath).getName();
String fileMd5 = calculateMD5(filePath);
String fileSize = getFileSize(filePath);
// 步骤3申请上传租约
System.out.println("步骤3向阿里云百炼申请上传租约");
ApplyFileUploadLeaseResponse leaseResponse = applyLease(client, categoryId, fileName, fileMd5, fileSize,
workspaceId);
String leaseId = leaseResponse.getBody().getData().getFileUploadLeaseId();
String uploadUrl = leaseResponse.getBody().getData().getParam().getUrl();
Object uploadHeaders = leaseResponse.getBody().getData().getParam().getHeaders();
// 步骤4上传文档
System.out.println("步骤4上传文档到阿里云百炼");
// 请自行安装jackson-databind
// 将上一步的uploadHeaders转换为Map(Key-Value形式)
ObjectMapper mapper = new ObjectMapper();
Map<String, String> uploadHeadersMap = (Map<String, String>) mapper
.readValue(mapper.writeValueAsString(uploadHeaders), Map.class);
uploadFile(uploadUrl, uploadHeadersMap, filePath);
// 步骤5将文档添加到服务器
System.out.println("步骤5将文档添加到阿里云百炼服务器");
AddFileResponse addResponse = addFile(client, leaseId, parser, categoryId, workspaceId);
String fileId = addResponse.getBody().getData().getFileId();
// 步骤6检查文档状态
System.out.println("步骤6检查阿里云百炼中的文档状态");
while (true) {
DescribeFileResponse describeResponse = describeFile(client, workspaceId, fileId);
String status = describeResponse.getBody().getData().getStatus();
System.out.println("当前文档状态:" + status);
if (status.equals("INIT")) {
System.out.println("文档待解析,请稍候...");
} else if (status.equals("PARSING")) {
System.out.println("文档解析中,请稍候...");
} else if (status.equals("PARSE_SUCCESS")) {
System.out.println("文档解析完成!");
break;
} else {
System.out.println("未知的文档状态:" + status + ",请联系技术支持。");
return null;
}
TimeUnit.SECONDS.sleep(5);
}
// 步骤7初始化知识库
System.out.println("步骤7在阿里云百炼中创建知识库");
CreateIndexResponse indexResponse = createIndex(client, workspaceId, fileId, name, structureType,
sourceType, sinkType);
String indexId = indexResponse.getBody().getData().getId();
// 步骤8提交索引任务
System.out.println("步骤8向阿里云百炼提交索引任务");
SubmitIndexJobResponse submitResponse = submitIndex(client, workspaceId, indexId);
String jobId = submitResponse.getBody().getData().getId();
// 步骤9获取索引任务状态
System.out.println("步骤9获取阿里云百炼索引任务状态");
while (true) {
GetIndexJobStatusResponse getStatusResponse = getIndexJobStatus(client, workspaceId, jobId, indexId);
String status = getStatusResponse.getBody().getData().getStatus();
System.out.println("当前索引任务状态:" + status);
if (status.equals("COMPLETED")) {
break;
}
TimeUnit.SECONDS.sleep(5);
}
System.out.println("阿里云百炼知识库创建成功!");
return indexId;
} catch (Exception e) {
System.out.println("发生错误:" + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
* 主函数。
*/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
if (!checkEnvironmentVariables()) {
return;
}
System.out.print("请输入您需要上传文档的实际本地路径以Linux为例/xxx/xxx/阿里云百炼系列手机产品介绍.docx");
String filePath = scanner.nextLine();
System.out.print("请为您的知识库输入一个名称:");
String kbName = scanner.nextLine();
String workspaceId = System.getenv("WORKSPACE_ID");
String result = createKnowledgeBase(filePath, workspaceId, kbName);
if (result != null) {
System.out.println("知识库ID: " + result);
}
}
}

View File

@@ -1,145 +0,0 @@
package com.gxwebsoft.ai.util;
import com.aliyun.bailian20231229.models.DeleteIndexResponse;
import com.aliyun.bailian20231229.models.ListIndicesResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
/**
* 管理知识库
* @author GIIT-YC
*
*/
public class KnowledgeBaseManage {
String ALIBABA_CLOUD_ACCESS_KEY_ID = "LTAI5tD5YRKuxWz6Eg7qrM4P";
String ALIBABA_CLOUD_ACCESS_KEY_SECRET = "bO8TBDXflOwbtSKimPpG8XrJnyzgTk";
String WORKSPACE_ID = "llm-4pf5auwewoz34zqu";
/**
* 检查并提示设置必要的环境变量。
*
* @return true 如果所有必需的环境变量都已设置,否则 false
*/
public static boolean checkEnvironmentVariables() {
Map<String, String> requiredVars = new HashMap<>();
requiredVars.put("ALIBABA_CLOUD_ACCESS_KEY_ID", "阿里云访问密钥ID");
requiredVars.put("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "阿里云访问密钥密码");
requiredVars.put("WORKSPACE_ID", "阿里云百炼业务空间ID");
List<String> missingVars = new ArrayList<>();
for (Map.Entry<String, String> entry : requiredVars.entrySet()) {
String value = System.getenv(entry.getKey());
if (value == null || value.isEmpty()) {
missingVars.add(entry.getKey());
System.out.println("错误:请设置 " + entry.getKey() + " 环境变量 (" + entry.getValue() + ")");
}
}
return missingVars.isEmpty();
}
/**
* 创建并配置客户端Client
*
* @return 配置好的客户端Client
*/
public static com.aliyun.bailian20231229.Client createClient() throws Exception {
com.aliyun.credentials.Client credential = new com.aliyun.credentials.Client();
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setCredential(credential);
// 下方接入地址以公有云的公网接入地址为例,可按需更换接入地址。
config.endpoint = "bailian.cn-beijing.aliyuncs.com";
return new com.aliyun.bailian20231229.Client(config);
}
/**
* 获取指定业务空间下一个或多个知识库的详细信息
*
* @param client 客户端Client
* @param workspaceId 业务空间ID
* @return 阿里云百炼服务的响应
*/
public static ListIndicesResponse listIndices(com.aliyun.bailian20231229.Client client, String workspaceId)
throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.bailian20231229.models.ListIndicesRequest listIndicesRequest = new com.aliyun.bailian20231229.models.ListIndicesRequest();
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
return client.listIndicesWithOptions(workspaceId, listIndicesRequest, headers, runtime);
}
/**
* 永久性删除指定的知识库
*
* @param client 客户端Client
* @param workspaceId 业务空间ID
* @param indexId 知识库ID
* @return 阿里云百炼服务的响应
*/
public static DeleteIndexResponse deleteIndex(com.aliyun.bailian20231229.Client client, String workspaceId,
String indexId) throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.bailian20231229.models.DeleteIndexRequest deleteIndexRequest = new com.aliyun.bailian20231229.models.DeleteIndexRequest();
deleteIndexRequest.setIndexId(indexId);
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
return client.deleteIndexWithOptions(workspaceId, deleteIndexRequest, headers, runtime);
}
/**
* 主函数
*/
public static void main(String[] args) {
if (!checkEnvironmentVariables()) {
System.out.println("环境变量校验未通过。");
return;
}
try {
Scanner scanner = new Scanner(System.in);
System.out.print("请选择要执行的操作:\n1. 查看知识库\n2. 删除知识库\n请输入选项1或2");
String startOption = scanner.nextLine();
com.aliyun.bailian20231229.Client client = createClient();
if (startOption.equals("1")) {
// 查看知识库
System.out.println("\n执行查看知识库");
String workspaceId = System.getenv("WORKSPACE_ID");
ListIndicesResponse response = listIndices(client, workspaceId);
// 请自行安装jackson-databind。将响应转换为 JSON 字符串
ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(response.getBody().getData());
System.out.println(result);
} else if (startOption.equals("2")) {
System.out.println("\n执行删除知识库");
String workspaceId = System.getenv("WORKSPACE_ID");
System.out.print("请输入知识库ID"); // 即 CreateIndex 接口返回的 Data.Id您也可以在阿里云百炼控制台的知识库页面获取。
String indexId = scanner.nextLine();
// 删除前二次确认
boolean confirm = false;
while (!confirm) {
System.out.print("您确定要永久性删除该知识库 " + indexId + " 吗?(y/n): ");
String input = scanner.nextLine().trim().toLowerCase();
if (input.equals("y")) {
confirm = true;
} else if (input.equals("n")) {
System.out.println("已取消删除操作。");
return;
} else {
System.out.println("无效输入,请输入 y 或 n。");
}
}
DeleteIndexResponse resp = deleteIndex(client, workspaceId, indexId);
if (resp.getBody().getStatus().equals("200")) {
System.out.println("知识库" + indexId + "删除成功!");
} else {
ObjectMapper mapper = new ObjectMapper();
System.out.println("发生错误:" + mapper.writeValueAsString(resp.getBody()));
}
} else {
System.out.println("无效的选项,程序退出。");
}
} catch (Exception e) {
System.out.println("发生错误:" + e.getMessage());
}
}
}

View File

@@ -1,110 +0,0 @@
package com.gxwebsoft.ai.util;
import com.aliyun.bailian20231229.models.RetrieveRequest;
import com.aliyun.bailian20231229.models.RetrieveResponse;
import com.aliyun.teautil.models.RuntimeOptions;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
/**
* 检索知识库
* @author GIIT-YC
*
*/
public class KnowledgeBaseRetrieve {
static String ALIBABA_CLOUD_ACCESS_KEY_ID = "LTAI5tD5YRKuxWz6Eg7qrM4P";
static String ALIBABA_CLOUD_ACCESS_KEY_SECRET = "bO8TBDXflOwbtSKimPpG8XrJnyzgTk";
static String WORKSPACE_ID = "llm-4pf5auwewoz34zqu";
/**
* 检查并提示设置必要的环境变量。
*
* @return true 如果所有必需的环境变量都已设置,否则 false
*/
public static boolean checkEnvironmentVariables() {
Map<String, String> requiredVars = new HashMap<>();
requiredVars.put("ALIBABA_CLOUD_ACCESS_KEY_ID", "阿里云访问密钥ID");
requiredVars.put("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "阿里云访问密钥密码");
requiredVars.put("WORKSPACE_ID", "阿里云百炼业务空间ID");
List<String> missingVars = new ArrayList<>();
for (Map.Entry<String, String> entry : requiredVars.entrySet()) {
String value = System.getenv(entry.getKey());
if (value == null || value.isEmpty()) {
missingVars.add(entry.getKey());
System.out.println("错误:请设置 " + entry.getKey() + " 环境变量 (" + entry.getValue() + ")");
}
}
return missingVars.isEmpty();
}
/**
* 初始化客户端Client
*
* @return 配置好的客户端对象
*/
public static com.aliyun.bailian20231229.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setAccessKeyId(ALIBABA_CLOUD_ACCESS_KEY_ID)
.setAccessKeySecret(ALIBABA_CLOUD_ACCESS_KEY_SECRET);
// 下方接入地址以公有云的公网接入地址为例,可按需更换接入地址。
config.endpoint = "bailian.cn-beijing.aliyuncs.com";
return new com.aliyun.bailian20231229.Client(config);
}
/**
* 在指定的知识库中检索信息。
*
* @param client 客户端对象bailian20231229Client
* @param workspaceId 业务空间ID
* @param indexId 知识库ID
* @param query 检索查询语句
* @return 阿里云百炼服务的响应
*/
public static RetrieveResponse retrieveIndex(com.aliyun.bailian20231229.Client client, String workspaceId,
String indexId, String query) throws Exception {
RetrieveRequest retrieveRequest = new RetrieveRequest();
retrieveRequest.setIndexId(indexId);
retrieveRequest.setQuery(query);
retrieveRequest.setDenseSimilarityTopK(null);
RuntimeOptions runtime = new RuntimeOptions();
return client.retrieveWithOptions(workspaceId, retrieveRequest, null, runtime);
}
/**
* 使用阿里云百炼服务检索知识库。
*/
public static void main(String[] args) {
// if (!checkEnvironmentVariables()) {
// System.out.println("环境变量校验未通过。");
// return;
// }
try {
// 步骤1初始化客户端Client
System.out.println("步骤1创建Client");
com.aliyun.bailian20231229.Client client = createClient();
// 步骤2检索知识库
System.out.println("步骤2检索知识库");
Scanner scanner = new Scanner(System.in);
System.out.print("请输入知识库ID"); // 即 CreateIndex 接口返回的 Data.Id您也可以在阿里云百炼控制台的知识库页面获取。
String indexId = scanner.nextLine();
System.out.print("请输入检索query");
String query = scanner.nextLine();
String workspaceId = WORKSPACE_ID;
RetrieveResponse resp = retrieveIndex(client, workspaceId, indexId, query);
// 请自行安装jackson-databind。将响应体responsebody转换为 JSON 字符串
ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(resp.getBody());
System.out.println(result);
} catch (Exception e) {
System.out.println("发生错误:" + e.getMessage());
}
}
}

View File

@@ -1,384 +0,0 @@
package com.gxwebsoft.ai.util;
import com.aliyun.bailian20231229.models.*;
import com.aliyun.teautil.models.RuntimeOptions;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.FileInputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.*;
/**
* 更新知识库
* @author GIIT-YC
*
*/
public class KnowledgeBaseUpdate {
String ALIBABA_CLOUD_ACCESS_KEY_ID = "LTAI5tD5YRKuxWz6Eg7qrM4P";
String ALIBABA_CLOUD_ACCESS_KEY_SECRET = "bO8TBDXflOwbtSKimPpG8XrJnyzgTk";
String WORKSPACE_ID = "llm-4pf5auwewoz34zqu";
/**
* 检查并提示设置必要的环境变量。
*
* @return true 如果所有必需的环境变量都已设置,否则 false
*/
public static boolean checkEnvironmentVariables() {
Map<String, String> requiredVars = new HashMap<>();
requiredVars.put("ALIBABA_CLOUD_ACCESS_KEY_ID", "阿里云访问密钥ID");
requiredVars.put("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "阿里云访问密钥密码");
requiredVars.put("WORKSPACE_ID", "阿里云百炼业务空间ID");
List<String> missingVars = new ArrayList<>();
for (Map.Entry<String, String> entry : requiredVars.entrySet()) {
String value = System.getenv(entry.getKey());
if (value == null || value.isEmpty()) {
missingVars.add(entry.getKey());
System.out.println("错误:请设置 " + entry.getKey() + " 环境变量 (" + entry.getValue() + ")");
}
}
return missingVars.isEmpty();
}
/**
* 创建并配置客户端Client
*
* @return 配置好的客户端Client
*/
public static com.aliyun.bailian20231229.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
.setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));
// 下方接入地址以公有云的公网接入地址为例,可按需更换接入地址。
config.endpoint = "bailian.cn-beijing.aliyuncs.com";
return new com.aliyun.bailian20231229.Client(config);
}
/**
* 计算文档的MD5值
*
* @param filePath 文档本地路径
* @return 文档的MD5值
*/
public static String calculateMD5(String filePath) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
try (FileInputStream fis = new FileInputStream(filePath)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
md.update(buffer, 0, bytesRead);
}
}
StringBuilder sb = new StringBuilder();
for (byte b : md.digest()) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
/**
* 获取文档大小(以字节为单位)
*
* @param filePath 文档本地路径
* @return 文档大小(以字节为单位)
*/
public static String getFileSize(String filePath) {
File file = new File(filePath);
long fileSize = file.length();
return String.valueOf(fileSize);
}
/**
* 申请文档上传租约。
*
* @param client 客户端对象
* @param categoryId 类目ID
* @param fileName 文档名称
* @param fileMd5 文档的MD5值
* @param fileSize 文档大小(以字节为单位)
* @param workspaceId 业务空间ID
* @return 阿里云百炼服务的响应对象
*/
public static ApplyFileUploadLeaseResponse applyLease(com.aliyun.bailian20231229.Client client, String categoryId,
String fileName, String fileMd5, String fileSize, String workspaceId) throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.bailian20231229.models.ApplyFileUploadLeaseRequest applyFileUploadLeaseRequest = new com.aliyun.bailian20231229.models.ApplyFileUploadLeaseRequest();
applyFileUploadLeaseRequest.setFileName(fileName);
applyFileUploadLeaseRequest.setMd5(fileMd5);
applyFileUploadLeaseRequest.setSizeInBytes(fileSize);
RuntimeOptions runtime = new RuntimeOptions();
ApplyFileUploadLeaseResponse applyFileUploadLeaseResponse = null;
applyFileUploadLeaseResponse = client.applyFileUploadLeaseWithOptions(categoryId, workspaceId,
applyFileUploadLeaseRequest, headers, runtime);
return applyFileUploadLeaseResponse;
}
/**
* 上传文档到临时存储。
*
* @param preSignedUrl 上传租约中的 URL
* @param headers 上传请求的头部
* @param filePath 文档本地路径
* @throws Exception 如果上传过程中发生错误
*/
public static void uploadFile(String preSignedUrl, Map<String, String> headers, String filePath) throws Exception {
File file = new File(filePath);
if (!file.exists() || !file.isFile()) {
throw new IllegalArgumentException("文件不存在或不是普通文件: " + filePath);
}
try (FileInputStream fis = new FileInputStream(file)) {
URL url = new URL(preSignedUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("PUT");
conn.setDoOutput(true);
// 设置上传请求头
conn.setRequestProperty("X-bailian-extra", headers.get("X-bailian-extra"));
conn.setRequestProperty("Content-Type", headers.get("Content-Type"));
// 分块读取并上传文档
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
conn.getOutputStream().write(buffer, 0, bytesRead);
}
int responseCode = conn.getResponseCode();
if (responseCode != 200) {
throw new RuntimeException("上传失败: " + responseCode);
}
}
}
/**
* 将文档添加到类目中。
*
* @param client 客户端对象
* @param leaseId 租约ID
* @param parser 用于文档的解析器
* @param categoryId 类目ID
* @param workspaceId 业务空间ID
* @return 阿里云百炼服务的响应对象
*/
public static AddFileResponse addFile(com.aliyun.bailian20231229.Client client, String leaseId, String parser,
String categoryId, String workspaceId) throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.bailian20231229.models.AddFileRequest addFileRequest = new com.aliyun.bailian20231229.models.AddFileRequest();
addFileRequest.setLeaseId(leaseId);
addFileRequest.setParser(parser);
addFileRequest.setCategoryId(categoryId);
RuntimeOptions runtime = new RuntimeOptions();
return client.addFileWithOptions(workspaceId, addFileRequest, headers, runtime);
}
/**
* 查询文档的基本信息。
*
* @param client 客户端对象
* @param workspaceId 业务空间ID
* @param fileId 文档ID
* @return 阿里云百炼服务的响应对象
*/
public static DescribeFileResponse describeFile(com.aliyun.bailian20231229.Client client, String workspaceId,
String fileId) throws Exception {
Map<String, String> headers = new HashMap<>();
RuntimeOptions runtime = new RuntimeOptions();
return client.describeFileWithOptions(workspaceId, fileId, headers, runtime);
}
/**
* 向一个非结构化知识库追加导入已解析的文档
*
* @param client 客户端Client
* @param workspaceId 业务空间ID
* @param indexId 知识库ID
* @param fileId 文档ID
* @param sourceType 数据类型
* @return 阿里云百炼服务的响应
*/
public static SubmitIndexAddDocumentsJobResponse submitIndexAddDocumentsJob(
com.aliyun.bailian20231229.Client client, String workspaceId, String indexId, String fileId,
String sourceType) throws Exception {
Map<String, String> headers = new HashMap<>();
SubmitIndexAddDocumentsJobRequest submitIndexAddDocumentsJobRequest = new SubmitIndexAddDocumentsJobRequest();
submitIndexAddDocumentsJobRequest.setIndexId(indexId);
submitIndexAddDocumentsJobRequest.setDocumentIds(Collections.singletonList(fileId));
submitIndexAddDocumentsJobRequest.setSourceType(sourceType);
RuntimeOptions runtime = new RuntimeOptions();
return client.submitIndexAddDocumentsJobWithOptions(workspaceId, submitIndexAddDocumentsJobRequest, headers,
runtime);
}
/**
* 查询索引任务状态。
*
* @param client 客户端对象
* @param workspaceId 业务空间ID
* @param jobId 任务ID
* @param indexId 知识库ID
* @return 阿里云百炼服务的响应对象
*/
public static GetIndexJobStatusResponse getIndexJobStatus(com.aliyun.bailian20231229.Client client,
String workspaceId, String jobId, String indexId) throws Exception {
Map<String, String> headers = new HashMap<>();
com.aliyun.bailian20231229.models.GetIndexJobStatusRequest getIndexJobStatusRequest = new com.aliyun.bailian20231229.models.GetIndexJobStatusRequest();
getIndexJobStatusRequest.setIndexId(indexId);
getIndexJobStatusRequest.setJobId(jobId);
RuntimeOptions runtime = new RuntimeOptions();
GetIndexJobStatusResponse getIndexJobStatusResponse = null;
getIndexJobStatusResponse = client.getIndexJobStatusWithOptions(workspaceId, getIndexJobStatusRequest, headers,
runtime);
return getIndexJobStatusResponse;
}
/**
* 从指定的非结构化知识库中永久删除一个或多个文档
*
* @param client 客户端Client
* @param workspaceId 业务空间ID
* @param indexId 知识库ID
* @param fileId 文档ID
* @return 阿里云百炼服务的响应
*/
public static DeleteIndexDocumentResponse deleteIndexDocument(com.aliyun.bailian20231229.Client client,
String workspaceId, String indexId, String fileId) throws Exception {
Map<String, String> headers = new HashMap<>();
DeleteIndexDocumentRequest deleteIndexDocumentRequest = new DeleteIndexDocumentRequest();
deleteIndexDocumentRequest.setIndexId(indexId);
deleteIndexDocumentRequest.setDocumentIds(Collections.singletonList(fileId));
RuntimeOptions runtime = new RuntimeOptions();
return client.deleteIndexDocumentWithOptions(workspaceId, deleteIndexDocumentRequest, headers, runtime);
}
/**
* 使用阿里云百炼服务更新知识库
*
* @param filePath 文档(更新后的)的实际本地路径
* @param workspaceId 业务空间ID
* @param indexId 需要更新的知识库ID
* @param oldFileId 需要更新的文档的FileID
* @return 如果成功返回知识库ID否则返回 null
*/
public static String updateKnowledgeBase(String filePath, String workspaceId, String indexId, String oldFileId) {
// 设置默认值
String categoryId = "default";
String parser = "DASHSCOPE_DOCMIND";
String sourceType = "DATA_CENTER_FILE";
try {
// 步骤1初始化客户端Client
System.out.println("步骤1创建Client");
com.aliyun.bailian20231229.Client client = createClient();
// 步骤2准备文档信息更新后的文档
System.out.println("步骤2准备文档信息");
String fileName = Paths.get(filePath).getFileName().toString();
String fileMd5 = calculateMD5(filePath);
String fileSize = getFileSize(filePath);
// 步骤3申请上传租约
System.out.println("步骤3向阿里云百炼申请上传租约");
ApplyFileUploadLeaseResponse leaseResponse = applyLease(client, categoryId, fileName, fileMd5, fileSize,
workspaceId);
String leaseId = leaseResponse.getBody().getData().getFileUploadLeaseId();
String uploadUrl = leaseResponse.getBody().getData().getParam().getUrl();
Object uploadHeaders = leaseResponse.getBody().getData().getParam().getHeaders();
// 步骤4上传文档到临时存储
System.out.println("步骤4上传文档到临时存储");
// 请自行安装jackson-databind
// 将上一步的uploadHeaders转换为Map(Key-Value形式)
ObjectMapper mapper = new ObjectMapper();
Map<String, String> uploadHeadersMap = (Map<String, String>) mapper
.readValue(mapper.writeValueAsString(uploadHeaders), Map.class);
uploadFile(uploadUrl, uploadHeadersMap, filePath);
// 步骤5添加文档到类目中
System.out.println("步骤5添加文档到类目中");
AddFileResponse addResponse = addFile(client, leaseId, parser, categoryId, workspaceId);
String fileId = addResponse.getBody().getData().getFileId();
// 步骤6检查更新后的文档状态
System.out.println("步骤6检查阿里云百炼中的文档状态");
while (true) {
DescribeFileResponse describeResponse = describeFile(client, workspaceId, fileId);
String status = describeResponse.getBody().getData().getStatus();
System.out.println("当前文档状态:" + status);
if ("INIT".equals(status)) {
System.out.println("文档待解析,请稍候...");
} else if ("PARSING".equals(status)) {
System.out.println("文档解析中,请稍候...");
} else if ("PARSE_SUCCESS".equals(status)) {
System.out.println("文档解析完成!");
break;
} else {
System.out.println("未知的文档状态:" + status + ",请联系技术支持。");
return null;
}
Thread.sleep(5000);
}
// 步骤7提交追加文档任务
System.out.println("步骤7提交追加文档任务");
SubmitIndexAddDocumentsJobResponse indexAddResponse = submitIndexAddDocumentsJob(client, workspaceId,
indexId, fileId, sourceType);
String jobId = indexAddResponse.getBody().getData().getId();
// 步骤8等待追加任务完成
System.out.println("步骤8等待追加任务完成");
while (true) {
GetIndexJobStatusResponse jobStatusResponse = getIndexJobStatus(client, workspaceId, jobId, indexId);
String status = jobStatusResponse.getBody().getData().getStatus();
System.out.println("当前索引任务状态:" + status);
if ("COMPLETED".equals(status)) {
break;
}
Thread.sleep(5000);
}
// 步骤9删除旧文档
System.out.println("步骤9删除旧文档");
deleteIndexDocument(client, workspaceId, indexId, oldFileId);
System.out.println("阿里云百炼知识库更新成功!");
return indexId;
} catch (Exception e) {
System.out.println("发生错误:" + e.getMessage());
return null;
}
}
/**
* 主函数。
*/
public static void main(String[] args) {
if (!checkEnvironmentVariables()) {
System.out.println("环境变量校验未通过。");
return;
}
Scanner scanner = new Scanner(System.in);
System.out.print("请输入您需要上传文档更新后的的实际本地路径以Linux为例/xxx/xxx/阿里云百炼系列手机产品介绍.docx");
String filePath = scanner.nextLine();
System.out.print("请输入需要更新的知识库ID"); // 即 CreateIndex 接口返回的 Data.Id您也可以在阿里云百炼控制台的知识库页面获取。
String indexId = scanner.nextLine(); // 即 AddFile 接口返回的 FileId。您也可以在阿里云百炼控制台的应用数据页面单击文件名称旁的 ID 图标获取。
System.out.print("请输入需要更新的文档的 FileID");
String oldFileId = scanner.nextLine();
String workspaceId = System.getenv("WORKSPACE_ID");
String result = updateKnowledgeBase(filePath, workspaceId, indexId, oldFileId);
if (result != null) {
System.out.println("知识库更新成功返回知识库ID: " + result);
} else {
System.out.println("知识库更新失败。");
}
}
}

View File

@@ -1,13 +1,17 @@
package com.gxwebsoft.cms.controller; package com.gxwebsoft.cms.controller;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
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.cms.entity.CmsDesign; import com.gxwebsoft.cms.entity.CmsDesign;
import com.gxwebsoft.cms.entity.CmsModel; import com.gxwebsoft.cms.entity.CmsModel;
import com.gxwebsoft.cms.param.CmsNavigationImportParam;
import com.gxwebsoft.cms.service.CmsDesignService; import com.gxwebsoft.cms.service.CmsDesignService;
import com.gxwebsoft.cms.service.CmsModelService; import com.gxwebsoft.cms.service.CmsModelService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.utils.CommonUtil; import com.gxwebsoft.common.core.utils.CommonUtil;
import com.gxwebsoft.common.core.utils.RedisUtil; import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
@@ -21,11 +25,18 @@ import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService; import com.gxwebsoft.common.system.service.UserService;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 网站导航记录表控制器 * 网站导航记录表控制器
@@ -79,6 +90,7 @@ public class CmsNavigationController extends BaseController {
return success(cmsNavigationService.getByIdRelByCodeRel(code)); return success(cmsNavigationService.getByIdRelByCodeRel(code));
} }
@PreAuthorize("hasAuthority('cms:cmsNavigation:save')")
@Operation(summary = "添加网站导航记录表") @Operation(summary = "添加网站导航记录表")
@PostMapping() @PostMapping()
public ApiResult<?> save(@RequestBody CmsNavigation cmsNavigation) { public ApiResult<?> save(@RequestBody CmsNavigation cmsNavigation) {
@@ -99,6 +111,7 @@ public class CmsNavigationController extends BaseController {
return fail("添加失败"); return fail("添加失败");
} }
@PreAuthorize("hasAuthority('cms:cmsNavigation:update')")
@Operation(summary = "修改网站导航记录表") @Operation(summary = "修改网站导航记录表")
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody CmsNavigation cmsNavigation) { public ApiResult<?> update(@RequestBody CmsNavigation cmsNavigation) {
@@ -111,6 +124,7 @@ public class CmsNavigationController extends BaseController {
return fail("修改失败"); return fail("修改失败");
} }
@PreAuthorize("hasAuthority('cms:cmsNavigation:remove')")
@Operation(summary = "删除网站导航记录表") @Operation(summary = "删除网站导航记录表")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
@@ -121,6 +135,7 @@ public class CmsNavigationController extends BaseController {
return fail("删除失败"); return fail("删除失败");
} }
@PreAuthorize("hasAuthority('cms:cmsNavigation:save')")
@Operation(summary = "批量添加网站导航记录表") @Operation(summary = "批量添加网站导航记录表")
@PostMapping("/batch") @PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<CmsNavigation> list) { public ApiResult<?> saveBatch(@RequestBody List<CmsNavigation> list) {
@@ -131,6 +146,7 @@ public class CmsNavigationController extends BaseController {
return fail("添加失败"); return fail("添加失败");
} }
@PreAuthorize("hasAuthority('cms:cmsNavigation:update')")
@Operation(summary = "批量修改网站导航记录表") @Operation(summary = "批量修改网站导航记录表")
@PutMapping("/batch") @PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<CmsNavigation> batchParam) { public ApiResult<?> removeBatch(@RequestBody BatchParam<CmsNavigation> batchParam) {
@@ -141,6 +157,7 @@ public class CmsNavigationController extends BaseController {
return fail("修改失败"); return fail("修改失败");
} }
@PreAuthorize("hasAuthority('cms:cmsNavigation:remove')")
@Operation(summary = "批量删除网站导航记录表") @Operation(summary = "批量删除网站导航记录表")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
@@ -151,6 +168,173 @@ public class CmsNavigationController extends BaseController {
return fail("删除失败"); return fail("删除失败");
} }
/**
* excel批量导入网站导航
*/
@PreAuthorize("hasAuthority('cms:cmsNavigation:save')")
@OperationLog
@Operation(summary = "批量导入网站导航")
@Transactional(rollbackFor = {Exception.class})
@PostMapping("/import")
public ApiResult<?> importBatch(@RequestParam("file") MultipartFile file) {
ImportParams importParams = new ImportParams();
try {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
Integer currentUserId = loginUser.getUserId();
Integer currentTenantId = loginUser.getTenantId();
// 1) 清理当前租户的历史数据:先清理 deleted=1再把 deleted=0 标记为 deleted=1
List<CmsNavigation> undeleted = cmsNavigationService.list(new LambdaQueryWrapper<CmsNavigation>()
.eq(CmsNavigation::getTenantId, currentTenantId)
.eq(CmsNavigation::getDeleted, 0));
cmsNavigationService.remove(new LambdaQueryWrapper<CmsNavigation>()
.eq(CmsNavigation::getTenantId, currentTenantId)
.eq(CmsNavigation::getDeleted, 1));
if (!CollectionUtils.isEmpty(undeleted)) {
LambdaUpdateWrapper<CmsNavigation> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(CmsNavigation::getTenantId, currentTenantId);
updateWrapper.eq(CmsNavigation::getDeleted, 0);
updateWrapper.set(CmsNavigation::getDeleted, 1);
cmsNavigationService.update(updateWrapper);
}
// 2) 读取Excel
List<CmsNavigationImportParam> list = ExcelImportUtil.importExcel(
file.getInputStream(), CmsNavigationImportParam.class, importParams);
if (CollectionUtils.isEmpty(list)) {
return fail("未读取到数据,请确认模板表头与示例格式一致");
}
// 3) 先全部落库(先按根节点保存),再尝试按“导入文件的 parentId -> 导入文件的 navigationId”映射回填层级。
// 这样即使 parentId 无法匹配导入文件缺少导航ID/父节点缺失),也能把数据全部导入,无法还原层级的作为根节点处理。
Map<Integer, Integer> newIdByOldId = new HashMap<>();
List<Integer> newIds = new ArrayList<>(list.size());
List<Integer> oldIds = new ArrayList<>(list.size());
List<Integer> oldParentIds = new ArrayList<>(list.size());
for (CmsNavigationImportParam param : list) {
CmsNavigation nav = convertToNavigation(param, currentUserId, currentTenantId);
nav.setParentId(0);
cmsNavigationService.save(nav);
cmsNavigationService.saveAsync(nav);
Integer oldId = param.getNavigationId();
if (oldId != null) {
newIdByOldId.put(oldId, nav.getNavigationId());
}
newIds.add(nav.getNavigationId());
oldIds.add(oldId);
oldParentIds.add(param.getParentId() != null ? param.getParentId() : 0);
}
int orphanCount = 0;
int restoredCount = 0;
for (int i = 0; i < newIds.size(); i++) {
Integer oldParentId = oldParentIds.get(i);
if (oldParentId == null || oldParentId == 0) {
continue;
}
Integer oldId = oldIds.get(i);
Integer newId = newIds.get(i);
Integer newParentId = newIdByOldId.get(oldParentId);
// 无法匹配父节点(或出现自引用)就当作孤儿节点,保持根节点
if (newParentId == null || (oldId != null && oldParentId.equals(oldId)) || (newId != null && newParentId.equals(newId))) {
orphanCount++;
continue;
}
cmsNavigationService.update(new LambdaUpdateWrapper<CmsNavigation>()
.eq(CmsNavigation::getNavigationId, newId)
.set(CmsNavigation::getParentId, newParentId));
restoredCount++;
}
redisUtil.delete(SITE_INFO_KEY_PREFIX.concat(currentTenantId.toString()));
return success("成功导入" + list.size() + "条,恢复层级" + restoredCount + "条,无法还原层级的孤儿节点" + orphanCount + "条(已作为根节点导入)");
} catch (Exception e) {
e.printStackTrace();
return fail("导入失败: " + e.getMessage());
}
}
/**
* 递归创建子级导航
*/
private void createChildNavigations(Map<Integer, List<CmsNavigationImportParam>> navGroups,
Map<Integer, CmsNavigation> tempIdMapping,
Integer originalParentId,
Integer defaultUserId,
Integer defaultTenantId) {
if (originalParentId == null || originalParentId == 0) {
return;
}
List<CmsNavigationImportParam> children = navGroups.get(originalParentId);
if (CollectionUtils.isEmpty(children)) {
return;
}
CmsNavigation parent = tempIdMapping.get(originalParentId);
if (parent == null || parent.getNavigationId() == null) {
return;
}
Integer newParentId = parent.getNavigationId();
for (CmsNavigationImportParam param : children) {
CmsNavigation nav = convertToNavigation(param, defaultUserId, defaultTenantId);
nav.setParentId(newParentId);
cmsNavigationService.save(nav);
cmsNavigationService.saveAsync(nav);
if (param.getNavigationId() != null) {
tempIdMapping.put(param.getNavigationId(), nav);
}
createChildNavigations(navGroups, tempIdMapping, param.getNavigationId(), defaultUserId, defaultTenantId);
}
}
private CmsNavigation convertToNavigation(CmsNavigationImportParam param, Integer defaultUserId, Integer defaultTenantId) {
CmsNavigation nav = new CmsNavigation();
nav.setType(param.getType());
nav.setTitle(StrUtil.trimStart(param.getTitle()));
nav.setParentId(param.getParentId() != null ? param.getParentId() : 0);
// saveAsync 依赖 model 生成路由/页面;缺省按 page 处理
nav.setModel(StrUtil.isBlank(param.getModel()) ? "page" : param.getModel());
nav.setCode(param.getCode());
nav.setPath(param.getPath());
nav.setComponent(param.getComponent());
nav.setTarget(param.getTarget());
nav.setIcon(param.getIcon());
nav.setColor(param.getColor());
nav.setHide(param.getHide());
nav.setPermission(param.getPermission());
nav.setPassword(param.getPassword());
nav.setPosition(param.getPosition());
nav.setTop(param.getTop());
nav.setBottom(param.getBottom());
nav.setActive(param.getActive());
nav.setMeta(param.getMeta());
nav.setStyle(param.getStyle());
nav.setModelName(param.getModelName());
nav.setPageId(param.getPageId());
nav.setItemId(param.getItemId());
nav.setIsMpWeixin(param.getIsMpWeixin());
nav.setGutter(param.getGutter());
nav.setSpan(param.getSpan());
nav.setReadNum(param.getReadNum());
nav.setMerchantId(param.getMerchantId());
nav.setLang(param.getLang());
nav.setHome(param.getHome());
nav.setRecommend(param.getRecommend());
nav.setSortNumber(param.getSortNumber() != null ? param.getSortNumber() : 0);
nav.setComments(param.getComments());
nav.setStatus(param.getStatus() != null ? param.getStatus() : 0);
// 导入时强制落到当前登录用户/租户,避免“导出其他租户 -> 导入”写回原租户导致当前租户数据不全。
nav.setUserId(defaultUserId);
nav.setTenantId(defaultTenantId);
nav.setDeleted(0);
return nav;
}
@Operation(summary = "获取树形结构的网站导航数据") @Operation(summary = "获取树形结构的网站导航数据")
@GetMapping("/tree") @GetMapping("/tree")
public ApiResult<List<CmsNavigation>> tree(CmsNavigationParam param) { public ApiResult<List<CmsNavigation>> tree(CmsNavigationParam param) {

View File

@@ -0,0 +1,122 @@
package com.gxwebsoft.cms.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.cms.service.CmsOrderService;
import com.gxwebsoft.cms.entity.CmsOrder;
import com.gxwebsoft.cms.param.CmsOrderParam;
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 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-01-27 13:03:25
*/
@Tag(name = "网站订单管理")
@RestController
@RequestMapping("/api/cms/cms-order")
public class CmsOrderController extends BaseController {
@Resource
private CmsOrderService cmsOrderService;
@PreAuthorize("hasAuthority('cms:cmsOrder:list')")
@Operation(summary = "分页查询网站订单")
@GetMapping("/page")
public ApiResult<PageResult<CmsOrder>> page(CmsOrderParam param) {
// 使用关联查询
return success(cmsOrderService.pageRel(param));
}
@PreAuthorize("hasAuthority('cms:cmsOrder:list')")
@Operation(summary = "查询全部网站订单")
@GetMapping()
public ApiResult<List<CmsOrder>> list(CmsOrderParam param) {
// 使用关联查询
return success(cmsOrderService.listRel(param));
}
@PreAuthorize("hasAuthority('cms:cmsOrder:list')")
@Operation(summary = "根据id查询网站订单")
@GetMapping("/{id}")
public ApiResult<CmsOrder> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(cmsOrderService.getByIdRel(id));
}
@Operation(summary = "添加网站订单")
@PostMapping()
public ApiResult<?> save(@RequestBody CmsOrder cmsOrder) {
if (cmsOrderService.save(cmsOrder)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('cms:cmsOrder:update')")
@OperationLog
@Operation(summary = "修改网站订单")
@PutMapping()
public ApiResult<?> update(@RequestBody CmsOrder cmsOrder) {
if (cmsOrderService.updateById(cmsOrder)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('cms:cmsOrder:remove')")
@OperationLog
@Operation(summary = "删除网站订单")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (cmsOrderService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('cms:cmsOrder:save')")
@OperationLog
@Operation(summary = "批量添加网站订单")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<CmsOrder> list) {
if (cmsOrderService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('cms:cmsOrder:update')")
@OperationLog
@Operation(summary = "批量修改网站订单")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<CmsOrder> batchParam) {
if (batchParam.update(cmsOrderService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('cms:cmsOrder:remove')")
@OperationLog
@Operation(summary = "批量删除网站订单")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (cmsOrderService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -43,6 +43,10 @@ public class CmsNavigation implements Serializable {
@Schema(description = "模型") @Schema(description = "模型")
private String model; private String model;
@Schema(description = "模型名称")
@TableField(exist = false)
private String modelName;
@Schema(description = "标识") @Schema(description = "标识")
private String code; private String code;
@@ -114,9 +118,6 @@ public class CmsNavigation implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private Integer parentPosition; private Integer parentPosition;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "绑定的页面(已废弃)") @Schema(description = "绑定的页面(已废弃)")
private Integer pageId; private Integer pageId;

View File

@@ -0,0 +1,266 @@
package com.gxwebsoft.cms.entity;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableLogic;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 网站订单
*
* @author 科技小王子
* @since 2026-01-27 13:03:24
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "CmsOrder对象", description = "网站订单")
public class CmsOrder implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "订单类型0售前咨询 1售后服务 2意见反馈")
private Integer type;
@Schema(description = "订单标题")
private String title;
@Schema(description = "公司/团队名称")
private String company;
@Schema(description = "订单内容")
private String content;
@Schema(description = "订单编号")
private String orderNo;
@Schema(description = "快递/自提")
private Integer deliveryType;
@Schema(description = "下单渠道0网站 1微信小程序 2其他")
private Integer channel;
@Schema(description = "微信支付交易号号")
private String transactionId;
@Schema(description = "微信退款订单号")
private String refundOrder;
@Schema(description = "商户ID")
private Integer merchantId;
@Schema(description = "商户名称")
private String merchantName;
@Schema(description = "商户编号")
private String merchantCode;
@Schema(description = "使用的优惠券id")
private Integer couponId;
@Schema(description = "使用的会员卡id")
private String cardId;
@Schema(description = "关联管理员id")
private Integer adminId;
@Schema(description = "核销管理员id")
private Integer confirmId;
@Schema(description = "IC卡号")
private String icCard;
@Schema(description = "真实姓名")
private String realName;
@Schema(description = "关联收货地址")
private Integer addressId;
@Schema(description = "收货地址")
private String address;
private String addressLat;
private String addressLng;
@Schema(description = "自提店铺id")
private Integer selfTakeMerchantId;
@Schema(description = "自提店铺")
private String selfTakeMerchantName;
@Schema(description = "配送开始时间")
private String sendStartTime;
@Schema(description = "配送结束时间")
private String sendEndTime;
@Schema(description = "发货店铺id")
private Integer expressMerchantId;
@Schema(description = "发货店铺")
private String expressMerchantName;
@Schema(description = "订单总额")
private BigDecimal totalPrice;
@Schema(description = "减少的金额使用VIP会员折扣、优惠券抵扣、优惠券折扣后减去的价格")
private BigDecimal reducePrice;
@Schema(description = "实际付款")
private BigDecimal payPrice;
@Schema(description = "用于统计")
private BigDecimal price;
@Schema(description = "价钱,用于积分赠送")
private BigDecimal money;
@Schema(description = "取消时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime cancelTime;
@Schema(description = "取消原因")
private String cancelReason;
@Schema(description = "退款金额")
private BigDecimal refundMoney;
@Schema(description = "教练价格")
private BigDecimal coachPrice;
@Schema(description = "购买数量")
private Integer totalNum;
@Schema(description = "教练id")
private Integer coachId;
@Schema(description = "商品ID")
private Integer formId;
@Schema(description = "支付的用户id")
private Integer payUserId;
@Schema(description = "0余额支付1微信支付2支付宝支付3银联支付4现金支付5POS机支付6免费7积分支付")
private Integer payType;
@Schema(description = "微信支付子类型JSAPI小程序支付NATIVE扫码支付")
private String wechatPayType;
@Schema(description = "0余额支付1微信支付2支付宝支付3银联支付4现金支付5POS机支付6免费7积分支付")
private Integer friendPayType;
@Schema(description = "0未付款1已付款")
private Integer payStatus;
@Schema(description = "0未使用1已完成2已取消3取消中4退款申请中5退款被拒绝6退款成功7客户端申请退款")
private Integer orderStatus;
@Schema(description = "发货状态(10未发货 20已发货 30部分发货)")
private Integer deliveryStatus;
@Schema(description = "无需发货备注")
private String deliveryNote;
@Schema(description = "发货时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime deliveryTime;
@Schema(description = "评价状态(0未评价 1已评价)")
private Integer evaluateStatus;
@Schema(description = "评价时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime evaluateTime;
@Schema(description = "优惠类型0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡")
private Integer couponType;
@Schema(description = "优惠说明")
private String couponDesc;
@Schema(description = "二维码地址,保存订单号,支付成功后才生成")
private String qrcode;
@Schema(description = "vip月卡年卡、ic月卡年卡回退次数")
private Integer returnNum;
@Schema(description = "vip充值回退金额")
private BigDecimal returnMoney;
@Schema(description = "预约详情开始时间数组")
private String startTime;
@Schema(description = "是否已开具发票0未开发票1已开发票2不能开具发票")
private Integer isInvoice;
@Schema(description = "发票流水号")
private String invoiceNo;
@Schema(description = "商家留言")
private String merchantRemarks;
@Schema(description = "支付时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime payTime;
@Schema(description = "退款时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime refundTime;
@Schema(description = "申请退款时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime refundApplyTime;
@Schema(description = "过期时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime expirationTime;
@Schema(description = "自提码")
private String selfTakeCode;
@Schema(description = "是否已收到赠品")
private Boolean hasTakeGift;
@Schema(description = "对账情况0=未对账1=已对账3=已对账金额对不上4=未查询到该订单")
private Integer checkBill;
@Schema(description = "订单是否已结算(0未结算 1已结算)")
private Integer isSettled;
@Schema(description = "系统版本号 0当前版本 value=其他版本")
private Integer version;
@Schema(description = "用户id")
private Integer userId;
@Schema(description = "备注")
private String comments;
@Schema(description = "排序号")
private Integer sortNumber;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.cms.entity.CmsOrder;
import com.gxwebsoft.cms.param.CmsOrderParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 网站订单Mapper
*
* @author 科技小王子
* @since 2026-01-27 13:03:24
*/
public interface CmsOrderMapper extends BaseMapper<CmsOrder> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<CmsOrder>
*/
List<CmsOrder> selectPageRel(@Param("page") IPage<CmsOrder> page,
@Param("param") CmsOrderParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<CmsOrder> selectListRel(@Param("param") CmsOrderParam param);
}

View File

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

View File

@@ -0,0 +1,258 @@
<?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.cms.mapper.CmsOrderMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*
FROM cms_order a
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.type != null">
AND a.type = #{param.type}
</if>
<if test="param.title != null">
AND a.title LIKE CONCAT('%', #{param.title}, '%')
</if>
<if test="param.company != null">
AND a.company LIKE CONCAT('%', #{param.company}, '%')
</if>
<if test="param.content != null">
AND a.content LIKE CONCAT('%', #{param.content}, '%')
</if>
<if test="param.orderNo != null">
AND a.order_no LIKE CONCAT('%', #{param.orderNo}, '%')
</if>
<if test="param.deliveryType != null">
AND a.delivery_type = #{param.deliveryType}
</if>
<if test="param.channel != null">
AND a.channel = #{param.channel}
</if>
<if test="param.transactionId != null">
AND a.transaction_id LIKE CONCAT('%', #{param.transactionId}, '%')
</if>
<if test="param.refundOrder != null">
AND a.refund_order LIKE CONCAT('%', #{param.refundOrder}, '%')
</if>
<if test="param.merchantId != null">
AND a.merchant_id = #{param.merchantId}
</if>
<if test="param.merchantName != null">
AND a.merchant_name LIKE CONCAT('%', #{param.merchantName}, '%')
</if>
<if test="param.merchantCode != null">
AND a.merchant_code LIKE CONCAT('%', #{param.merchantCode}, '%')
</if>
<if test="param.couponId != null">
AND a.coupon_id = #{param.couponId}
</if>
<if test="param.cardId != null">
AND a.card_id LIKE CONCAT('%', #{param.cardId}, '%')
</if>
<if test="param.adminId != null">
AND a.admin_id = #{param.adminId}
</if>
<if test="param.confirmId != null">
AND a.confirm_id = #{param.confirmId}
</if>
<if test="param.icCard != null">
AND a.ic_card LIKE CONCAT('%', #{param.icCard}, '%')
</if>
<if test="param.realName != null">
AND a.real_name LIKE CONCAT('%', #{param.realName}, '%')
</if>
<if test="param.addressId != null">
AND a.address_id = #{param.addressId}
</if>
<if test="param.address != null">
AND a.address LIKE CONCAT('%', #{param.address}, '%')
</if>
<if test="param.addressLat != null">
AND a.address_lat LIKE CONCAT('%', #{param.addressLat}, '%')
</if>
<if test="param.addressLng != null">
AND a.address_lng LIKE CONCAT('%', #{param.addressLng}, '%')
</if>
<if test="param.selfTakeMerchantId != null">
AND a.self_take_merchant_id = #{param.selfTakeMerchantId}
</if>
<if test="param.selfTakeMerchantName != null">
AND a.self_take_merchant_name LIKE CONCAT('%', #{param.selfTakeMerchantName}, '%')
</if>
<if test="param.sendStartTime != null">
AND a.send_start_time LIKE CONCAT('%', #{param.sendStartTime}, '%')
</if>
<if test="param.sendEndTime != null">
AND a.send_end_time LIKE CONCAT('%', #{param.sendEndTime}, '%')
</if>
<if test="param.expressMerchantId != null">
AND a.express_merchant_id = #{param.expressMerchantId}
</if>
<if test="param.expressMerchantName != null">
AND a.express_merchant_name LIKE CONCAT('%', #{param.expressMerchantName}, '%')
</if>
<if test="param.totalPrice != null">
AND a.total_price = #{param.totalPrice}
</if>
<if test="param.reducePrice != null">
AND a.reduce_price = #{param.reducePrice}
</if>
<if test="param.payPrice != null">
AND a.pay_price = #{param.payPrice}
</if>
<if test="param.price != null">
AND a.price = #{param.price}
</if>
<if test="param.money != null">
AND a.money = #{param.money}
</if>
<if test="param.cancelTime != null">
AND a.cancel_time LIKE CONCAT('%', #{param.cancelTime}, '%')
</if>
<if test="param.cancelReason != null">
AND a.cancel_reason LIKE CONCAT('%', #{param.cancelReason}, '%')
</if>
<if test="param.refundMoney != null">
AND a.refund_money = #{param.refundMoney}
</if>
<if test="param.coachPrice != null">
AND a.coach_price = #{param.coachPrice}
</if>
<if test="param.totalNum != null">
AND a.total_num = #{param.totalNum}
</if>
<if test="param.coachId != null">
AND a.coach_id = #{param.coachId}
</if>
<if test="param.formId != null">
AND a.form_id = #{param.formId}
</if>
<if test="param.payUserId != null">
AND a.pay_user_id = #{param.payUserId}
</if>
<if test="param.payType != null">
AND a.pay_type = #{param.payType}
</if>
<if test="param.wechatPayType != null">
AND a.wechat_pay_type LIKE CONCAT('%', #{param.wechatPayType}, '%')
</if>
<if test="param.friendPayType != null">
AND a.friend_pay_type = #{param.friendPayType}
</if>
<if test="param.payStatus != null">
AND a.pay_status = #{param.payStatus}
</if>
<if test="param.orderStatus != null">
AND a.order_status = #{param.orderStatus}
</if>
<if test="param.deliveryStatus != null">
AND a.delivery_status = #{param.deliveryStatus}
</if>
<if test="param.deliveryNote != null">
AND a.delivery_note LIKE CONCAT('%', #{param.deliveryNote}, '%')
</if>
<if test="param.deliveryTime != null">
AND a.delivery_time LIKE CONCAT('%', #{param.deliveryTime}, '%')
</if>
<if test="param.evaluateStatus != null">
AND a.evaluate_status = #{param.evaluateStatus}
</if>
<if test="param.evaluateTime != null">
AND a.evaluate_time LIKE CONCAT('%', #{param.evaluateTime}, '%')
</if>
<if test="param.couponType != null">
AND a.coupon_type = #{param.couponType}
</if>
<if test="param.couponDesc != null">
AND a.coupon_desc LIKE CONCAT('%', #{param.couponDesc}, '%')
</if>
<if test="param.qrcode != null">
AND a.qrcode LIKE CONCAT('%', #{param.qrcode}, '%')
</if>
<if test="param.returnNum != null">
AND a.return_num = #{param.returnNum}
</if>
<if test="param.returnMoney != null">
AND a.return_money = #{param.returnMoney}
</if>
<if test="param.startTime != null">
AND a.start_time LIKE CONCAT('%', #{param.startTime}, '%')
</if>
<if test="param.isInvoice != null">
AND a.is_invoice = #{param.isInvoice}
</if>
<if test="param.invoiceNo != null">
AND a.invoice_no LIKE CONCAT('%', #{param.invoiceNo}, '%')
</if>
<if test="param.merchantRemarks != null">
AND a.merchant_remarks LIKE CONCAT('%', #{param.merchantRemarks}, '%')
</if>
<if test="param.payTime != null">
AND a.pay_time LIKE CONCAT('%', #{param.payTime}, '%')
</if>
<if test="param.refundTime != null">
AND a.refund_time LIKE CONCAT('%', #{param.refundTime}, '%')
</if>
<if test="param.refundApplyTime != null">
AND a.refund_apply_time LIKE CONCAT('%', #{param.refundApplyTime}, '%')
</if>
<if test="param.expirationTime != null">
AND a.expiration_time LIKE CONCAT('%', #{param.expirationTime}, '%')
</if>
<if test="param.selfTakeCode != null">
AND a.self_take_code LIKE CONCAT('%', #{param.selfTakeCode}, '%')
</if>
<if test="param.hasTakeGift != null">
AND a.has_take_gift = #{param.hasTakeGift}
</if>
<if test="param.checkBill != null">
AND a.check_bill = #{param.checkBill}
</if>
<if test="param.isSettled != null">
AND a.is_settled = #{param.isSettled}
</if>
<if test="param.version != null">
AND a.version = #{param.version}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.comments != null">
AND a.comments LIKE CONCAT('%', #{param.comments}, '%')
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</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.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}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.cms.entity.CmsOrder">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.cms.entity.CmsOrder">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -0,0 +1,123 @@
package com.gxwebsoft.cms.param;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import java.io.Serializable;
/**
* 网站导航导入参数Excel 批量导入)
*/
@Data
public class CmsNavigationImportParam implements Serializable {
private static final long serialVersionUID = 1L;
@Excel(name = "导航ID")
private Integer navigationId;
@Excel(name = "类型")
private Integer type;
@Excel(name = "菜单名称")
private String title;
@Excel(name = "上级id")
private Integer parentId;
@Excel(name = "模型")
private String model;
@Excel(name = "标识")
private String code;
@Excel(name = "菜单路由地址")
private String path;
@Excel(name = "菜单组件地址")
private String component;
@Excel(name = "打开位置")
private String target;
@Excel(name = "菜单图标")
private String icon;
@Excel(name = "图标颜色")
private String color;
@Excel(name = "是否隐藏")
private Integer hide;
@Excel(name = "可见类型")
private Integer permission;
@Excel(name = "访问密码")
private String password;
@Excel(name = "位置")
private Integer position;
@Excel(name = "仅在顶部显示")
private Integer top;
@Excel(name = "仅在底部显示")
private Integer bottom;
@Excel(name = "选中path")
private String active;
@Excel(name = "其它路由元信息")
private String meta;
@Excel(name = "css样式")
private String style;
@Excel(name = "模型名称")
private String modelName;
@Excel(name = "页面ID")
private Integer pageId;
@Excel(name = "详情页ID")
private Integer itemId;
@Excel(name = "是否微信小程序菜单")
private Boolean isMpWeixin;
@Excel(name = "菜单间距")
private Integer gutter;
@Excel(name = "菜单宽度")
private Integer span;
@Excel(name = "阅读量")
private Integer readNum;
@Excel(name = "商户ID")
private Long merchantId;
@Excel(name = "国际化语言")
private String lang;
@Excel(name = "设为首页")
private Integer home;
@Excel(name = "是否推荐")
private Boolean recommend;
@Excel(name = "排序")
private Integer sortNumber;
@Excel(name = "备注")
private String comments;
@Excel(name = "状态")
private Integer status;
@Excel(name = "用户ID")
private Integer userId;
@Excel(name = "租户ID")
private Integer tenantId;
}

View File

@@ -0,0 +1,284 @@
package com.gxwebsoft.cms.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-01-27 13:03:24
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "CmsOrderParam对象", description = "网站订单查询参数")
public class CmsOrderParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
@QueryField(type = QueryType.EQ)
private Integer id;
@Schema(description = "订单类型0售前咨询 1售后服务 2意见反馈")
@QueryField(type = QueryType.EQ)
private Integer type;
@Schema(description = "订单标题")
private String title;
@Schema(description = "公司/团队名称")
private String company;
@Schema(description = "订单内容")
private String content;
@Schema(description = "订单编号")
private String orderNo;
@Schema(description = "快递/自提")
@QueryField(type = QueryType.EQ)
private Integer deliveryType;
@Schema(description = "下单渠道0网站 1微信小程序 2其他")
@QueryField(type = QueryType.EQ)
private Integer channel;
@Schema(description = "微信支付交易号号")
private String transactionId;
@Schema(description = "微信退款订单号")
private String refundOrder;
@Schema(description = "商户ID")
@QueryField(type = QueryType.EQ)
private Long merchantId;
@Schema(description = "商户名称")
private String merchantName;
@Schema(description = "商户编号")
private String merchantCode;
@Schema(description = "使用的优惠券id")
@QueryField(type = QueryType.EQ)
private Integer couponId;
@Schema(description = "使用的会员卡id")
private String cardId;
@Schema(description = "关联管理员id")
@QueryField(type = QueryType.EQ)
private Integer adminId;
@Schema(description = "核销管理员id")
@QueryField(type = QueryType.EQ)
private Integer confirmId;
@Schema(description = "IC卡号")
private String icCard;
@Schema(description = "真实姓名")
private String realName;
@Schema(description = "关联收货地址")
@QueryField(type = QueryType.EQ)
private Integer addressId;
@Schema(description = "收货地址")
private String address;
private String addressLat;
private String addressLng;
@Schema(description = "自提店铺id")
@QueryField(type = QueryType.EQ)
private Integer selfTakeMerchantId;
@Schema(description = "自提店铺")
private String selfTakeMerchantName;
@Schema(description = "配送开始时间")
private String sendStartTime;
@Schema(description = "配送结束时间")
private String sendEndTime;
@Schema(description = "发货店铺id")
@QueryField(type = QueryType.EQ)
private Integer expressMerchantId;
@Schema(description = "发货店铺")
private String expressMerchantName;
@Schema(description = "订单总额")
@QueryField(type = QueryType.EQ)
private BigDecimal totalPrice;
@Schema(description = "减少的金额使用VIP会员折扣、优惠券抵扣、优惠券折扣后减去的价格")
@QueryField(type = QueryType.EQ)
private BigDecimal reducePrice;
@Schema(description = "实际付款")
@QueryField(type = QueryType.EQ)
private BigDecimal payPrice;
@Schema(description = "用于统计")
@QueryField(type = QueryType.EQ)
private BigDecimal price;
@Schema(description = "价钱,用于积分赠送")
@QueryField(type = QueryType.EQ)
private BigDecimal money;
@Schema(description = "取消时间")
private String cancelTime;
@Schema(description = "取消原因")
private String cancelReason;
@Schema(description = "退款金额")
@QueryField(type = QueryType.EQ)
private BigDecimal refundMoney;
@Schema(description = "教练价格")
@QueryField(type = QueryType.EQ)
private BigDecimal coachPrice;
@Schema(description = "购买数量")
@QueryField(type = QueryType.EQ)
private Integer totalNum;
@Schema(description = "教练id")
@QueryField(type = QueryType.EQ)
private Integer coachId;
@Schema(description = "商品ID")
@QueryField(type = QueryType.EQ)
private Integer formId;
@Schema(description = "支付的用户id")
@QueryField(type = QueryType.EQ)
private Integer payUserId;
@Schema(description = "0余额支付1微信支付2支付宝支付3银联支付4现金支付5POS机支付6免费7积分支付")
@QueryField(type = QueryType.EQ)
private Integer payType;
@Schema(description = "微信支付子类型JSAPI小程序支付NATIVE扫码支付")
private String wechatPayType;
@Schema(description = "0余额支付1微信支付2支付宝支付3银联支付4现金支付5POS机支付6免费7积分支付")
@QueryField(type = QueryType.EQ)
private Integer friendPayType;
@Schema(description = "0未付款1已付款")
@QueryField(type = QueryType.EQ)
private Integer payStatus;
@Schema(description = "0未使用1已完成2已取消3取消中4退款申请中5退款被拒绝6退款成功7客户端申请退款")
@QueryField(type = QueryType.EQ)
private Integer orderStatus;
@Schema(description = "发货状态(10未发货 20已发货 30部分发货)")
@QueryField(type = QueryType.EQ)
private Integer deliveryStatus;
@Schema(description = "无需发货备注")
private String deliveryNote;
@Schema(description = "发货时间")
private String deliveryTime;
@Schema(description = "评价状态(0未评价 1已评价)")
@QueryField(type = QueryType.EQ)
private Integer evaluateStatus;
@Schema(description = "评价时间")
private String evaluateTime;
@Schema(description = "优惠类型0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡")
@QueryField(type = QueryType.EQ)
private Integer couponType;
@Schema(description = "优惠说明")
private String couponDesc;
@Schema(description = "二维码地址,保存订单号,支付成功后才生成")
private String qrcode;
@Schema(description = "vip月卡年卡、ic月卡年卡回退次数")
@QueryField(type = QueryType.EQ)
private Integer returnNum;
@Schema(description = "vip充值回退金额")
@QueryField(type = QueryType.EQ)
private BigDecimal returnMoney;
@Schema(description = "预约详情开始时间数组")
private String startTime;
@Schema(description = "是否已开具发票0未开发票1已开发票2不能开具发票")
@QueryField(type = QueryType.EQ)
private Integer isInvoice;
@Schema(description = "发票流水号")
private String invoiceNo;
@Schema(description = "商家留言")
private String merchantRemarks;
@Schema(description = "支付时间")
private String payTime;
@Schema(description = "退款时间")
private String refundTime;
@Schema(description = "申请退款时间")
private String refundApplyTime;
@Schema(description = "过期时间")
private String expirationTime;
@Schema(description = "自提码")
private String selfTakeCode;
@Schema(description = "是否已收到赠品")
@QueryField(type = QueryType.EQ)
private Boolean hasTakeGift;
@Schema(description = "对账情况0=未对账1=已对账3=已对账金额对不上4=未查询到该订单")
@QueryField(type = QueryType.EQ)
private Integer checkBill;
@Schema(description = "订单是否已结算(0未结算 1已结算)")
@QueryField(type = QueryType.EQ)
private Integer isSettled;
@Schema(description = "系统版本号 0当前版本 value=其他版本")
@QueryField(type = QueryType.EQ)
private Integer version;
@Schema(description = "用户id")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "备注")
private String comments;
@Schema(description = "排序号")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@Schema(description = "是否删除, 0否, 1是")
@QueryField(type = QueryType.EQ)
private Integer deleted;
}

View File

@@ -0,0 +1,42 @@
package com.gxwebsoft.cms.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.cms.entity.CmsOrder;
import com.gxwebsoft.cms.param.CmsOrderParam;
import java.util.List;
/**
* 网站订单Service
*
* @author 科技小王子
* @since 2026-01-27 13:03:24
*/
public interface CmsOrderService extends IService<CmsOrder> {
/**
* 分页关联查询
*
* @param param 查询参数
* @return PageResult<CmsOrder>
*/
PageResult<CmsOrder> pageRel(CmsOrderParam param);
/**
* 关联查询全部
*
* @param param 查询参数
* @return List<CmsOrder>
*/
List<CmsOrder> listRel(CmsOrderParam param);
/**
* 根据id查询
*
* @param id id
* @return CmsOrder
*/
CmsOrder getByIdRel(Integer id);
}

View File

@@ -0,0 +1,47 @@
package com.gxwebsoft.cms.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.cms.mapper.CmsOrderMapper;
import com.gxwebsoft.cms.service.CmsOrderService;
import com.gxwebsoft.cms.entity.CmsOrder;
import com.gxwebsoft.cms.param.CmsOrderParam;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 网站订单Service实现
*
* @author 科技小王子
* @since 2026-01-27 13:03:24
*/
@Service
public class CmsOrderServiceImpl extends ServiceImpl<CmsOrderMapper, CmsOrder> implements CmsOrderService {
@Override
public PageResult<CmsOrder> pageRel(CmsOrderParam param) {
PageParam<CmsOrder, CmsOrderParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<CmsOrder> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<CmsOrder> listRel(CmsOrderParam param) {
List<CmsOrder> list = baseMapper.selectListRel(param);
// 排序
PageParam<CmsOrder, CmsOrderParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time desc");
return page.sortRecords(list);
}
@Override
public CmsOrder getByIdRel(Integer id) {
CmsOrderParam param = new CmsOrderParam();
param.setId(id);
return param.getOne(baseMapper.selectListRel(param));
}
}

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

@@ -1,11 +1,14 @@
package com.gxwebsoft.common.core.config; package com.gxwebsoft.common.core.config;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException; import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
/** /**
@@ -16,14 +19,43 @@ import java.time.format.DateTimeFormatter;
*/ */
public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> { public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private final DateTimeFormatter formatter;
public LocalDateTimeDeserializer() {
this(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
public LocalDateTimeDeserializer(DateTimeFormatter formatter) {
this.formatter = formatter;
}
@Override @Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = p.getValueAsString(); JsonToken t = p.currentToken();
if (value != null && !value.isEmpty()) { ZoneId zoneId = ctxt.getTimeZone() != null ? ctxt.getTimeZone().toZoneId() : ZoneId.systemDefault();
return LocalDateTime.parse(value, FORMATTER);
// Accept epoch timestamps (seconds or millis) for compatibility with frontends that send numbers.
if (t == JsonToken.VALUE_NUMBER_INT) {
long ts = p.getLongValue();
// Heuristic: 10+ digits is seconds; 13+ digits is millis (most common in JS).
Instant instant = (String.valueOf(Math.abs(ts)).length() >= 13)
? Instant.ofEpochMilli(ts)
: Instant.ofEpochSecond(ts);
return LocalDateTime.ofInstant(instant, zoneId);
} }
return null;
String value = p.getValueAsString();
if (value == null || value.isEmpty()) {
return null;
}
// Handle numeric timestamps passed as strings, e.g. "1769618486000"
if (value.chars().allMatch(Character::isDigit)) {
long ts = Long.parseLong(value);
Instant instant = (value.length() >= 13) ? Instant.ofEpochMilli(ts) : Instant.ofEpochSecond(ts);
return LocalDateTime.ofInstant(instant, zoneId);
}
return LocalDateTime.parse(value, formatter);
} }
} }

View File

@@ -80,7 +80,8 @@ public class SwaggerConfig {
public GroupedOpenApi shopApi() { public GroupedOpenApi shopApi() {
return GroupedOpenApi.builder() return GroupedOpenApi.builder()
.group("shop") .group("shop")
.pathsToMatch("/api/shop/**") // 订单等用户侧接口在 shop 包内,但路径使用 /api/user/**(前端统一 user 侧 API 前缀)
.pathsToMatch("/api/shop/**", "/api/user/**")
.packagesToScan("com.gxwebsoft.shop") .packagesToScan("com.gxwebsoft.shop")
.build(); .build();
} }

View File

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

View File

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

View File

@@ -1,8 +1,13 @@
package com.gxwebsoft.common.core.utils; package com.gxwebsoft.common.core.utils;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.util.TimeZone;
/** /**
* JSON解析工具类 * JSON解析工具类
@@ -11,8 +16,21 @@ import com.fasterxml.jackson.databind.ObjectWriter;
* @since 2017-06-10 10:10:39 * @since 2017-06-10 10:10:39
*/ */
public class JSONUtil { public class JSONUtil {
private static final ObjectMapper objectMapper = new ObjectMapper(); /**
private static final ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter(); * 注意:不要直接 new ObjectMapper() 否则不支持 Java8 时间类型LocalDateTime 等)。
* 这里做最小可用配置,避免在 Redis/日志/签名等场景序列化失败。
*/
private static final ObjectMapper objectMapper;
private static final ObjectWriter objectWriter;
static {
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
}
/** /**
* 对象转json字符串 * 对象转json字符串

View File

@@ -0,0 +1,119 @@
/**
* Copyright 2013-2015 JueYue (qrb.jueyue@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gxwebsoft.common.core.web;
import cn.afterturn.easypoi.excel.entity.ExcelBaseParams;
import cn.afterturn.easypoi.handler.inter.IExcelVerifyHandler;
import lombok.Data;
/**
* 导入参数设置
*
* @author JueYue
* 2013-9-24
* @version 1.0
*/
@Data
public class ImportParams extends ExcelBaseParams {
public static final String SAVE_URL = "/excel/upload/excelUpload";
/**
* 表格标题行数,默认0
*/
private int titleRows = 0;
/**
* 表头行数,默认1
*/
private int headRows = 1;
/**
* 字段真正值和列标题之间的距离 默认0
*/
private int startRows = 0;
/**
* 主键设置,如何这个cell没有值,就跳过 或者认为这个是list的下面的值
* 大家不理解,去掉这个
*/
private Integer keyIndex = null;
/**
* 开始读取的sheet位置,默认为0
*/
private int startSheetIndex = 0;
/**
* 上传表格需要读取的sheet 数量,默认为1
*/
private int sheetNum = 1;
/**
* 是否需要保存上传的Excel,默认为false
*/
private boolean needSave = false;
/**
* 校验组
*/
private Class[] verifyGroup = null;
/**
* 是否需要校验上传的Excel,默认为false
*/
private boolean needVerify = false;
/**
* 返回文件是否分割,默认是分割
*/
private boolean verifyFileSplit = true;
/**
* 校验处理接口
*/
private IExcelVerifyHandler verifyHandler;
/**
* 保存上传的Excel目录,默认是 如 TestEntity这个类保存路径就是
* upload/excelUpload/Test/yyyyMMddHHmss_***** 保存名称上传时间_五位随机数
*/
private String saveUrl = SAVE_URL;
/**
* 最后的无效行数
*/
private int lastOfInvalidRow = 0;
/**
* 手动控制读取的行数
*/
private int readRows = 0;
/**
* 导入时校验数据模板,是不是正确的Excel
*/
private String[] importFields;
/**
* 导入时校验excel的标题列顺序。依赖于importFields的配置顺序
*/
private boolean needCheckOrder = false;
/**
* Key-Value 读取标记,以这个为Key,后面一个Cell 为Value,多个改为ArrayList
*/
private String keyMark = ":";
/**
* 按照Key-Value 规则读取全局扫描Excel,但是跳过List读取范围提升性能
* 仅仅支持titleRows + headRows + startRows 以及 lastOfInvalidRow
*/
private boolean readSingleCell = false;
/**
* 是否并行计算
*/
private boolean concurrentTask = false;
/**
* 最小截取大小
*/
private Integer critical = 1000;
}

View File

@@ -1,20 +1,29 @@
package com.gxwebsoft.common.system.controller; package com.gxwebsoft.common.system.controller;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
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.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.*; import com.gxwebsoft.common.core.web.*;
import com.gxwebsoft.common.system.entity.Menu; import com.gxwebsoft.common.system.entity.*;
import com.gxwebsoft.common.system.entity.Plug; import com.gxwebsoft.common.system.param.MenuImportParam;
import com.gxwebsoft.common.system.param.MenuParam; import com.gxwebsoft.common.system.param.MenuParam;
import com.gxwebsoft.common.system.service.MenuService; import com.gxwebsoft.common.system.param.VersionParam;
import com.gxwebsoft.common.system.service.PlugService; import com.gxwebsoft.common.system.service.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation; 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.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List; import java.util.*;
import java.util.stream.Collectors;
/** /**
* 菜单控制器 * 菜单控制器
@@ -22,21 +31,27 @@ import java.util.List;
* @author WebSoft * @author WebSoft
* @since 2018-12-24 16:10:23 * @since 2018-12-24 16:10:23
*/ */
@Tag(name = "菜单管理") @Tag(name = "菜单")
@RestController @RestController
@RequestMapping("/api/system/menu") @RequestMapping("/api/system/menu")
public class MenuController extends BaseController { public class MenuController extends BaseController {
@Resource @Resource
private MenuService menuService; private MenuService menuService;
@Resource @Resource
private PlugService plugService; private CompanyService companyService;
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private RoleMenuService roleMenuService;
@PreAuthorize("hasAuthority('sys:menu:list')") @PreAuthorize("hasAuthority('sys:menu:list')")
@Operation(summary = "分页查询菜单") @Operation(summary = "分页查询菜单")
@GetMapping("/page") @GetMapping("/page")
public ApiResult<PageResult<Menu>> page(MenuParam param) { public ApiResult<PageResult<Menu>> page(MenuParam param) {
PageParam<Menu, MenuParam> page = new PageParam<>(param); PageParam<Menu, MenuParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number"); page.setDefaultOrder("sort_number asc, create_time desc");
return success(menuService.page(page, page.getWrapper())); return success(menuService.page(page, page.getWrapper()));
} }
@@ -45,7 +60,7 @@ public class MenuController extends BaseController {
@GetMapping() @GetMapping()
public ApiResult<List<Menu>> list(MenuParam param) { public ApiResult<List<Menu>> list(MenuParam param) {
PageParam<Menu, MenuParam> page = new PageParam<>(param); PageParam<Menu, MenuParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number"); page.setDefaultOrder("sort_number asc, create_time desc");
return success(menuService.list(page.getOrderWrapper())); return success(menuService.list(page.getOrderWrapper()));
} }
@@ -57,12 +72,18 @@ public class MenuController extends BaseController {
} }
@PreAuthorize("hasAuthority('sys:menu:save')") @PreAuthorize("hasAuthority('sys:menu:save')")
@OperationLog
@Operation(summary = "添加菜单") @Operation(summary = "添加菜单")
@PostMapping() @PostMapping()
public ApiResult<?> add(@RequestBody Menu menu) { public ApiResult<?> add(@RequestBody Menu menu) {
if (menu.getParentId() == null) { if (menu.getParentId() == null) {
menu.setParentId(0); menu.setParentId(0);
} }
// 去除字符串前面的空格
menu.setTitle(StrUtil.trimStart(menu.getTitle()));
menu.setPath(StrUtil.trimStart(menu.getPath()));
menu.setComponent(StrUtil.trimStart(menu.getComponent()));
menu.setAuthority(StrUtil.trimStart(menu.getAuthority()));
if (menuService.save(menu)) { if (menuService.save(menu)) {
return success("添加成功"); return success("添加成功");
} }
@@ -70,9 +91,15 @@ public class MenuController extends BaseController {
} }
@PreAuthorize("hasAuthority('sys:menu:update')") @PreAuthorize("hasAuthority('sys:menu:update')")
@OperationLog
@Operation(summary = "修改菜单") @Operation(summary = "修改菜单")
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody Menu menu) { public ApiResult<?> update(@RequestBody Menu menu) {
// 去除字符串前面的空格
menu.setTitle(StrUtil.trimStart(menu.getTitle()));
menu.setPath(StrUtil.trimStart(menu.getPath()));
menu.setComponent(StrUtil.trimStart(menu.getComponent()));
menu.setAuthority(StrUtil.trimStart(menu.getAuthority()));
if (menuService.updateById(menu)) { if (menuService.updateById(menu)) {
return success("修改成功"); return success("修改成功");
} }
@@ -80,6 +107,7 @@ public class MenuController extends BaseController {
} }
@PreAuthorize("hasAuthority('sys:menu:remove')") @PreAuthorize("hasAuthority('sys:menu:remove')")
@OperationLog
@Operation(summary = "删除菜单") @Operation(summary = "删除菜单")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
@@ -90,6 +118,7 @@ public class MenuController extends BaseController {
} }
@PreAuthorize("hasAuthority('sys:menu:save')") @PreAuthorize("hasAuthority('sys:menu:save')")
@OperationLog
@Operation(summary = "批量添加菜单") @Operation(summary = "批量添加菜单")
@PostMapping("/batch") @PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<Menu> menus) { public ApiResult<?> saveBatch(@RequestBody List<Menu> menus) {
@@ -100,6 +129,7 @@ public class MenuController extends BaseController {
} }
@PreAuthorize("hasAuthority('sys:menu:update')") @PreAuthorize("hasAuthority('sys:menu:update')")
@OperationLog
@Operation(summary = "批量修改菜单") @Operation(summary = "批量修改菜单")
@PutMapping("/batch") @PutMapping("/batch")
public ApiResult<?> updateBatch(@RequestBody BatchParam<Menu> batchParam) { public ApiResult<?> updateBatch(@RequestBody BatchParam<Menu> batchParam) {
@@ -110,6 +140,7 @@ public class MenuController extends BaseController {
} }
@PreAuthorize("hasAuthority('sys:menu:remove')") @PreAuthorize("hasAuthority('sys:menu:remove')")
@OperationLog
@Operation(summary = "批量删除菜单") @Operation(summary = "批量删除菜单")
@DeleteMapping("/batch") @DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) { public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
@@ -119,14 +150,35 @@ public class MenuController extends BaseController {
return fail("删除失败"); return fail("删除失败");
} }
@PreAuthorize("hasAuthority('sys:menu:update')") @PreAuthorize("hasAuthority('sys:menu:remove')")
@Operation(summary = "菜单克隆") @Operation(summary = "删除父级以下菜单")
@PostMapping("/clone") @DeleteMapping("/deleteParentMenu/{id}")
public ApiResult<?> onClone(@RequestBody MenuParam param){ public ApiResult<?> deleteParentMenu(@PathVariable("id") Integer id) {
if(menuService.cloneMenu(param)){ final List<Menu> list = menuService.list(new LambdaQueryWrapper<Menu>().eq(Menu::getParentId, id));
return success("克隆成功,请刷新"); if (CollectionUtils.isEmpty(list)) {
menuService.removeById(id);
return success("删除成功");
} }
return fail("克隆失败"); final Set<Integer> ids = list.stream().map(Menu::getMenuId).collect(Collectors.toSet());
final List<Menu> list2 = menuService.list(new LambdaUpdateWrapper<Menu>().in(Menu::getParentId, ids));
final Set<Integer> collect = list2.stream().map(Menu::getMenuId).collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(list2)) {
ids.addAll(collect);
final List<Menu> list3 = menuService.list(new LambdaUpdateWrapper<Menu>().in(Menu::getParentId, ids));
final Set<Integer> collect1 = list3.stream().map(Menu::getMenuId).collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(collect1)) {
ids.addAll(collect1);
}
ids.add(id);
if (menuService.removeByIds(ids)) {
return success("删除成功");
}
}
ids.add(id);
if (menuService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
} }
@PreAuthorize("hasAuthority('sys:menu:update')") @PreAuthorize("hasAuthority('sys:menu:update')")
@@ -135,11 +187,271 @@ public class MenuController extends BaseController {
public ApiResult<?> install(@PathVariable("id") Integer id){ public ApiResult<?> install(@PathVariable("id") Integer id){
if(menuService.install(id)){ if(menuService.install(id)){
// 更新安装次数 // 更新安装次数
final Plug plug = plugService.getOne(new LambdaQueryWrapper<Plug>().eq(Plug::getMenuId, id)); // final Plug plug = plugService.getOne(new LambdaQueryWrapper<Plug>().eq(Plug::getMenuId, id));
plug.setInstalls(plug.getInstalls() + 1); // plug.setInstalls(plug.getInstalls() + 1);
plugService.updateById(plug); // plugService.updateById(plug);
return success("安装成功"); return success("安装成功");
} }
return fail("安装失败",id); return fail("安装失败",id);
} }
/**
* excel批量导入菜单
*/
@PreAuthorize("hasAuthority('sys:menu:save')")
@Operation(summary = "批量导入菜单")
@Transactional(rollbackFor = {Exception.class})
@PostMapping("/import")
public ApiResult<?> importBatch(MultipartFile file) {
ImportParams importParams = new ImportParams();
try {
System.out.println("=== 开始菜单导入流程 ===");
// 检查导入前的菜单数据
long beforeCount = menuService.count();
System.out.println("导入前菜单总数: " + beforeCount);
// 检查当前未删除的菜单
List<Menu> undeletedMenus = menuService.list(new LambdaQueryWrapper<Menu>().eq(Menu::getDeleted, 0));
System.out.println("当前未删除的菜单数: " + undeletedMenus.size());
if (!undeletedMenus.isEmpty()) {
System.out.println("未删除菜单列表:");
for (Menu menu : undeletedMenus) {
System.out.println(" ID: " + menu.getMenuId() + ", 名称: " + menu.getTitle() +
", 父级ID: " + menu.getParentId() + ", deleted: " + menu.getDeleted());
}
}
// 第一步:永久删除已标记为 deleted=1 的记录
boolean deleteResult = menuService.remove(new LambdaQueryWrapper<Menu>().eq(Menu::getDeleted, 1));
System.out.println("已永久删除标记为deleted=1的菜单记录结果: " + deleteResult);
// 第二步将现有未删除的记录deleted=0标记为 deleted=1
if (!undeletedMenus.isEmpty()) {
LambdaUpdateWrapper<Menu> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(Menu::getDeleted, 0);
updateWrapper.set(Menu::getDeleted, 1);
boolean updateResult = menuService.update(updateWrapper);
System.out.println("更新未删除菜单记录的结果: " + updateResult);
}
// 检查更新后的菜单数据
long afterCleanupCount = menuService.count(new LambdaQueryWrapper<Menu>().eq(Menu::getDeleted, 0));
System.out.println("清理后未标记删除的菜单数: " + afterCleanupCount);
// 第三步导入XLS文件的内容
List<MenuImportParam> list = ExcelImportUtil.importExcel(file.getInputStream(), MenuImportParam.class, importParams);
System.out.println("从Excel文件中读取到" + list.size() + "条菜单记录");
// 存储原始parentId到菜单列表的映射关系
Map<Integer, List<MenuImportParam>> menuGroups = new HashMap<>();
// 存储原始ID到新菜单对象的映射关系用于后续设置正确的parentId
Map<Integer, Menu> tempIdMapping = new HashMap<>();
// 按parentId分组处理null值
for (MenuImportParam param : list) {
Integer parentId = param.getParentId() != null ? param.getParentId() : 0;
menuGroups.computeIfAbsent(parentId, k -> new ArrayList<>()).add(param);
}
System.out.println("菜单分组情况:");
for (Map.Entry<Integer, List<MenuImportParam>> entry : menuGroups.entrySet()) {
System.out.println(" parentId=" + entry.getKey() + " 的菜单数: " + entry.getValue().size());
}
// 先创建所有父级菜单parentId为0的菜单
List<MenuImportParam> rootMenus = menuGroups.getOrDefault(0, new ArrayList<>());
List<Menu> createdRootMenus = new ArrayList<>();
System.out.println("开始创建" + rootMenus.size() + "个根菜单");
for (MenuImportParam param : rootMenus) {
Menu menu = convertToMenu(param);
menu.setParentId(0); // 根菜单的parentId为0
menuService.save(menu);
createdRootMenus.add(menu);
System.out.println("创建根菜单: " + menu.getTitle() + ", ID: " + menu.getMenuId() + ", 原始ID: " + param.getMenuId());
// 记录原始ID到新菜单的映射关系
if (param.getMenuId() != null) {
tempIdMapping.put(param.getMenuId(), menu);
}
}
// 递归创建子级菜单注意这里不再处理parentId=0的菜单因为已经在上面处理过了
System.out.println("开始创建子级菜单(跳过根菜单)");
// 只处理非根菜单的子级菜单
for (Map.Entry<Integer, List<MenuImportParam>> entry : menuGroups.entrySet()) {
Integer parentId = entry.getKey();
if (parentId != 0) { // 跳过根菜单parentId=0
System.out.println("处理parentId=" + parentId + "的子菜单");
createChildMenus(menuGroups, tempIdMapping, parentId);
}
}
// 获取所有导入的菜单ID
List<Integer> allImportedMenuIds = new ArrayList<>();
for (Menu menu : tempIdMapping.values()) {
allImportedMenuIds.add(menu.getMenuId());
}
System.out.println("总共导入了" + allImportedMenuIds.size() + "个菜单");
// 显示导入的菜单详情
if (!allImportedMenuIds.isEmpty()) {
List<Menu> allImportedMenus = menuService.list(new LambdaQueryWrapper<Menu>()
.in(Menu::getMenuId, allImportedMenuIds)
.orderByAsc(Menu::getParentId, Menu::getSortNumber));
System.out.println("导入的菜单详情:");
for (Menu menu : allImportedMenus) {
System.out.println(" ID: " + menu.getMenuId() + ", 名称: " + menu.getTitle() +
", 父级ID: " + menu.getParentId() + ", 类型: " + menu.getMenuType());
}
}
// 为超级管理员配置菜单权限
if (!allImportedMenuIds.isEmpty()) {
List<Menu> allImportedMenus = menuService.list(new LambdaQueryWrapper<Menu>()
.in(Menu::getMenuId, allImportedMenuIds));
System.out.println("" + allImportedMenus.size() + "个菜单配置超级管理员权限");
configureSuperAdminPermissionsForImportedMenus(allImportedMenus);
}
// 最终检查
long finalCount = menuService.count();
System.out.println("导入后菜单总数: " + finalCount);
System.out.println("=== 菜单导入流程结束 ===");
return success("成功导入" + list.size() + "");
} catch (Exception e) {
e.printStackTrace();
return fail("导入失败: " + e.getMessage());
}
}
/**
* 递归创建子级菜单
* @param menuGroups 菜单分组
* @param tempIdMapping 临时ID映射关系
* @param originalParentId 原始父级ID
*/
private void createChildMenus(Map<Integer, List<MenuImportParam>> menuGroups,
Map<Integer, Menu> tempIdMapping,
Integer originalParentId) {
System.out.println(">>> 进入createChildMenus方法处理originalParentId=" + originalParentId);
// 特殊处理originalParentId=0的情况已经在主方法中处理过了这里不应该再处理
if (originalParentId == 0) {
System.out.println(" 跳过originalParentId=0的处理已在主方法中处理");
System.out.println("<<< 退出createChildMenus方法处理originalParentId=" + originalParentId);
return;
}
List<MenuImportParam> childMenus = menuGroups.get(originalParentId);
if (childMenus == null || childMenus.isEmpty()) {
System.out.println(" 没有找到originalParentId=" + originalParentId + "的子菜单");
System.out.println("<<< 退出createChildMenus方法处理originalParentId=" + originalParentId);
return;
}
// 获取新的父级菜单对象
Menu parentMenu = tempIdMapping.get(originalParentId);
if (parentMenu == null) {
System.out.println(" 未找到原始ID为" + originalParentId + "的父级菜单,跳过处理");
System.out.println("<<< 退出createChildMenus方法处理originalParentId=" + originalParentId);
return;
}
Integer newParentId = parentMenu.getMenuId();
System.out.println(" 创建父级ID为" + originalParentId + "(新ID:" + newParentId + ")的子菜单,共" + childMenus.size() + "");
// 创建所有子菜单
for (int i = 0; i < childMenus.size(); i++) {
MenuImportParam param = childMenus.get(i);
System.out.println(" 处理第" + (i+1) + "个子菜单原始ID: " + param.getMenuId() + ", 原始ParentId: " + param.getParentId());
Menu menu = convertToMenu(param);
menu.setParentId(newParentId);
menuService.save(menu);
System.out.println(" 创建子菜单: " + menu.getTitle() + ", ID: " + menu.getMenuId() +
", 父级ID: " + menu.getParentId() + ", 原始ID: " + param.getMenuId());
// 记录原始ID到新菜单的映射关系
if (param.getMenuId() != null) {
tempIdMapping.put(param.getMenuId(), menu);
}
// 递归创建当前菜单的子级菜单使用原始ID作为下一个递归的父级ID
System.out.println(" 递归调用createChildMenus处理原始ID=" + param.getMenuId() + "的子菜单");
createChildMenus(menuGroups, tempIdMapping, param.getMenuId());
}
System.out.println("<<< 退出createChildMenus方法处理originalParentId=" + originalParentId);
}
/**
* 将MenuImportParam转换为Menu实体
* @param param MenuImportParam对象
* @return Menu实体
*/
private Menu convertToMenu(MenuImportParam param) {
Menu menu = new Menu();
menu.setParentId(param.getParentId());
menu.setTitle(param.getTitle());
menu.setPath(param.getPath());
menu.setComponent(param.getComponent());
menu.setModules(param.getModules());
menu.setModulesUrl(param.getModulesUrl());
menu.setMenuType(param.getMenuType());
menu.setSortNumber(param.getSortNumber() != null ? param.getSortNumber() : 0);
menu.setAuthority(param.getAuthority());
menu.setIcon(param.getIcon());
menu.setHide(param.getHide());
menu.setAppId(param.getAppId());
menu.setTenantId(param.getTenantId());
menu.setDeleted(0); // 新导入的数据deleted设为0
return menu;
}
/**
* 为超级管理员配置导入菜单的权限
* @param importedMenus 导入的菜单列表
*/
private void configureSuperAdminPermissionsForImportedMenus(List<Menu> importedMenus) {
try {
// 1.查找当前租户的超管权限的roleId
final Role superAdmin = roleService.getOne(new LambdaQueryWrapper<Role>().eq(Role::getRoleCode, "superAdmin"));
if (superAdmin == null) {
System.out.println("未找到superAdmin角色");
return;
}
final Integer roleId = superAdmin.getRoleId();
final Integer tenantId = superAdmin.getTenantId();
// 为所有导入的菜单配置权限
for (Menu menu : importedMenus) {
RoleMenu roleMenu = new RoleMenu();
roleMenu.setRoleId(roleId);
roleMenu.setMenuId(menu.getMenuId());
roleMenu.setTenantId(tenantId);
roleMenuService.save(roleMenu);
}
// 调整根菜单的排序(如果有根菜单的话)
for (Menu menu : importedMenus) {
if (menu.getParentId() == 0) {
menu.setSortNumber(0);
menuService.updateById(menu);
break;
}
}
System.out.println("为超级管理员配置菜单权限成功,共配置了" + importedMenus.size() + "个菜单");
} catch (Exception e) {
System.err.println("为超级管理员配置菜单权限失败: " + e.getMessage());
e.printStackTrace();
}
}
} }

View File

@@ -10,6 +10,8 @@ import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.gxwebsoft.cms.entity.CmsWebsite;
import com.gxwebsoft.cms.service.CmsWebsiteService;
import com.gxwebsoft.common.core.config.ConfigProperties; import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.exception.BusinessException; import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.security.JwtSubject; import com.gxwebsoft.common.core.security.JwtSubject;
@@ -40,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;
@@ -52,7 +53,7 @@ import static com.gxwebsoft.common.core.constants.RedisConstants.MP_WX_KEY;
public class WxLoginController extends BaseController { public class WxLoginController extends BaseController {
private final StringRedisTemplate redisTemplate; private final StringRedisTemplate redisTemplate;
private final OkHttpClient http = new OkHttpClient(); private final OkHttpClient http = new OkHttpClient();
private final ObjectMapper om = new ObjectMapper(); private final ObjectMapper om;
private volatile long tokenExpireEpoch = 0L; // 过期的 epoch 秒 private volatile long tokenExpireEpoch = 0L; // 过期的 epoch 秒
@Resource @Resource
private SettingService settingService; private SettingService settingService;
@@ -74,10 +75,13 @@ public class WxLoginController extends BaseController {
private ConfigProperties config; private ConfigProperties config;
@Resource @Resource
private UserRefereeService userRefereeService; private UserRefereeService userRefereeService;
@Resource
private CmsWebsiteService cmsWebsiteService;
public WxLoginController(StringRedisTemplate redisTemplate) { public WxLoginController(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) {
this.redisTemplate = redisTemplate; this.redisTemplate = redisTemplate;
this.om = objectMapper;
} }
@Operation(summary = "获取微信AccessToken") @Operation(summary = "获取微信AccessToken")
@@ -241,27 +245,30 @@ public class WxLoginController extends BaseController {
* @param userParam 需要传微信凭证code * @param userParam 需要传微信凭证code
*/ */
private String getPhoneByCode(UserParam userParam) { private String getPhoneByCode(UserParam userParam) {
// 获取手机号码
String apiUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + getAccessToken();
HashMap<String, Object> paramMap = new HashMap<>(); HashMap<String, Object> paramMap = new HashMap<>();
if (StrUtil.isBlank(userParam.getCode())) { if (StrUtil.isBlank(userParam.getCode())) {
throw new BusinessException("code不能为空"); throw new BusinessException("code不能为空");
} }
paramMap.put("code", userParam.getCode()); paramMap.put("code", userParam.getCode());
// 执行post请求
String post = HttpUtil.post(apiUrl, JSON.toJSONString(paramMap)); // access_token 失效/过期时自动刷新并重试一次
JSONObject json = JSON.parseObject(post); for (int attempt = 0; attempt < 2; attempt++) {
if (json.get("errcode").equals(0)) { String accessToken = (attempt == 0) ? getAccessToken(false) : getAccessToken(true);
JSONObject phoneInfo = JSON.parseObject(json.getString("phone_info")); String apiUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken;
// 微信用户的手机号码
final String phoneNumber = phoneInfo.getString("phoneNumber"); String post = HttpUtil.post(apiUrl, JSON.toJSONString(paramMap));
// 验证手机号码 JSONObject json = JSON.parseObject(post);
// if (userParam.getNotVerifyPhone() == null && !Validator.isMobile(phoneNumber)) {
// String key = ACCESS_TOKEN_KEY.concat(":").concat(getTenantId().toString()); Integer errcode = json.getInteger("errcode");
// redisTemplate.delete(key); if (errcode != null && errcode.equals(0)) {
// throw new BusinessException("手机号码格式不正确"); JSONObject phoneInfo = JSON.parseObject(json.getString("phone_info"));
// } return phoneInfo.getString("phoneNumber");
return phoneNumber; }
if (errcode != null && (errcode == 40001 || errcode == 42001 || errcode == 40014)) {
continue;
}
return null;
} }
return null; return null;
} }
@@ -280,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());
@@ -289,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");
@@ -300,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并更新")
@@ -442,6 +472,13 @@ public class WxLoginController extends BaseController {
map.put("page", "pages/index/index"); map.put("page", "pages/index/index");
map.put("env_version", "release"); map.put("env_version", "release");
// 判断应用运行状态
final CmsWebsite website = cmsWebsiteService.getByTenantId(tenantId);
if(website.getRunning().equals(2)){
map.put("check_path",false);
map.put("env_version","trial");
}
String jsonBody = JSON.toJSONString(map); String jsonBody = JSON.toJSONString(map);
System.out.println("请求的 JSON body = " + jsonBody); System.out.println("请求的 JSON body = " + jsonBody);
@@ -564,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;
@@ -674,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 + " ===");
@@ -736,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;
} }
} }
@@ -777,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);
@@ -788,14 +842,15 @@ public class WxLoginController extends BaseController {
if (json.containsKey("errcode")) { if (json.containsKey("errcode")) {
Integer errcode = json.getInteger("errcode"); Integer errcode = json.getInteger("errcode");
String errmsg = json.getString("errmsg"); String errmsg = json.getString("errmsg");
System.err.println("微信API错误 - errcode: " + errcode + ", errmsg: " + errmsg); if (errcode != null && errcode != 0) {
System.err.println("微信API错误 - errcode: " + errcode + ", errmsg: " + errmsg);
if (errcode == 40125) { if (errcode == 40125) {
throw new RuntimeException("微信AppSecret配置错误请检查并更新正确的AppSecret"); throw new RuntimeException("微信AppSecret配置错误请检查并更新正确的AppSecret");
} else if (errcode == 40013) { } else if (errcode == 40013) {
throw new RuntimeException("微信AppID配置错误请检查并更新正确的AppID"); throw new RuntimeException("微信AppID配置错误请检查并更新正确的AppID");
} else { } else {
throw new RuntimeException("微信API调用失败: " + errmsg + " (errcode: " + errcode + ")"); throw new RuntimeException("微信API调用失败: " + errmsg + " (errcode: " + errcode + ")");
}
} }
} }
@@ -804,7 +859,8 @@ public class WxLoginController extends BaseController {
Integer expiresIn = json.getInteger("expires_in"); Integer expiresIn = json.getInteger("expires_in");
// 缓存access_token存储完整JSON响应与getAccessToken方法保持一致 // 缓存access_token存储完整JSON响应与getAccessToken方法保持一致
redisUtil.set(key, result, (long) (expiresIn - 300), TimeUnit.SECONDS); long ttlSeconds = Math.max((expiresIn != null ? expiresIn : 7200) - 300L, 60L);
redisUtil.set(key, result, ttlSeconds, TimeUnit.SECONDS);
System.out.println("获取新的access_token成功租户ID: " + tenantId); System.out.println("获取新的access_token成功租户ID: " + tenantId);
return accessToken; return accessToken;

View File

@@ -1,13 +1,12 @@
package com.gxwebsoft.common.system.entity; package com.gxwebsoft.common.system.entity;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import java.time.LocalDateTime; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.List; import java.util.List;
/** /**
@@ -19,6 +18,7 @@ import java.util.List;
@Data @Data
@Schema(description = "菜单") @Schema(description = "菜单")
@TableName("sys_menu") @TableName("sys_menu")
@JsonIgnoreProperties(ignoreUnknown = true)
public class Menu implements GrantedAuthority { public class Menu implements GrantedAuthority {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
public static final int TYPE_MENU = 0; // 菜单类型 public static final int TYPE_MENU = 0; // 菜单类型
@@ -40,9 +40,19 @@ public class Menu implements GrantedAuthority {
@Schema(description = "菜单组件地址") @Schema(description = "菜单组件地址")
private String component; private String component;
@Schema(description = "模块ID")
private String modules;
@Schema(description = "模块API")
private String modulesUrl;
@Schema(description = "菜单类型, 0菜单, 1按钮") @Schema(description = "菜单类型, 0菜单, 1按钮")
private Integer menuType; private Integer menuType;
@Schema(description = "打开方式, 0当前页, 1新窗口")
@TableField(exist = false)
private Integer openType;
@Schema(description = "排序号") @Schema(description = "排序号")
private Integer sortNumber; private Integer sortNumber;
@@ -69,12 +79,10 @@ public class Menu implements GrantedAuthority {
private Integer tenantId; private Integer tenantId;
@Schema(description = "创建时间") @Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime;
private LocalDateTime createTime;
@Schema(description = "修改时间") @Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime;
private LocalDateTime updateTime;
@Schema(description = "子菜单") @Schema(description = "子菜单")
@TableField(exist = false) @TableField(exist = false)

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

@@ -3,7 +3,7 @@
<mapper namespace="com.gxwebsoft.common.system.mapper.UserRoleMapper"> <mapper namespace="com.gxwebsoft.common.system.mapper.UserRoleMapper">
<insert id="insertBatch"> <insert id="insertBatch">
INSERT INTO sys_user_role(user_id, role_id) VALUES INSERT INTO gxwebsoft_core.sys_user_role(user_id, role_id) VALUES
<foreach collection="roleIds" item="roleId" separator=","> <foreach collection="roleIds" item="roleId" separator=",">
(#{userId}, #{roleId}) (#{userId}, #{roleId})
</foreach> </foreach>
@@ -11,10 +11,10 @@
<select id="selectByUserId" resultType="com.gxwebsoft.common.system.entity.Role"> <select id="selectByUserId" resultType="com.gxwebsoft.common.system.entity.Role">
SELECT * SELECT *
FROM sys_role FROM gxwebsoft_core.sys_role
WHERE role_id IN ( WHERE role_id IN (
SELECT role_id SELECT role_id
FROM sys_user_role FROM gxwebsoft_core.sys_user_role
WHERE user_id = #{userId} WHERE user_id = #{userId}
) )
AND deleted = 0 AND deleted = 0
@@ -23,7 +23,7 @@
<select id="selectByUserIds" resultType="com.gxwebsoft.common.system.entity.Role"> <select id="selectByUserIds" resultType="com.gxwebsoft.common.system.entity.Role">
SELECT a.user_id, b.* SELECT a.user_id, b.*
FROM sys_user_role a FROM gxwebsoft_core.sys_user_role a
LEFT JOIN gxwebsoft_core.sys_role b ON a.role_id = b.role_id LEFT JOIN gxwebsoft_core.sys_role b ON a.role_id = b.role_id
WHERE a.user_id IN WHERE a.user_id IN
<foreach collection="userIds" open="(" close=")" separator="," item="userId"> <foreach collection="userIds" open="(" close=")" separator="," item="userId">

View File

@@ -0,0 +1,60 @@
package com.gxwebsoft.common.system.param;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import java.io.Serializable;
/**
* 菜单导入参数
*
* @author WebSoft
* @since 2025-09-30
*/
@Data
public class MenuImportParam implements Serializable {
private static final long serialVersionUID = 1L;
@Excel(name = "菜单ID")
private Integer menuId;
@Excel(name = "父级ID")
private Integer parentId;
@Excel(name = "菜单名称")
private String title;
@Excel(name = "路由地址")
private String path;
@Excel(name = "组件路径")
private String component;
@Excel(name = "模块ID")
private String modules;
@Excel(name = "模块API")
private String modulesUrl;
@Excel(name = "菜单类型")
private Integer menuType;
@Excel(name = "排序号")
private Integer sortNumber;
@Excel(name = "权限标识")
private String authority;
@Excel(name = "图标")
private String icon;
@Excel(name = "是否隐藏")
private Integer hide;
@Excel(name = "关联应用")
private Integer appId;
@Excel(name = "租户id")
private Integer tenantId;
}

View File

@@ -1,11 +1,11 @@
package com.gxwebsoft.common.system.param; package com.gxwebsoft.common.system.param;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableLogic;
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;
import com.gxwebsoft.common.core.web.BaseParam; import com.gxwebsoft.common.core.web.BaseParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -40,10 +40,17 @@ public class MenuParam extends BaseParam {
@Schema(description = "菜单组件地址") @Schema(description = "菜单组件地址")
private String component; private String component;
@Schema(description = "模块ID")
private String modules;
@Schema(description = "菜单类型, 0菜单, 1按钮") @Schema(description = "菜单类型, 0菜单, 1按钮")
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer menuType; private Integer menuType;
@Schema(description = "打开方式, 0当前页, 1新窗口")
@TableField(exist = false)
private Integer openType;
@Schema(description = "权限标识") @Schema(description = "权限标识")
private String authority; private String authority;
@@ -65,4 +72,12 @@ public class MenuParam extends BaseParam {
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer tenantId; private Integer tenantId;
@Schema(description = "企业ID")
@QueryField(type = QueryType.EQ)
private Integer companyId;
@Schema(description = "租户名称")
@TableField(exist = false)
private String tenantName;
} }

View File

@@ -0,0 +1,70 @@
package com.gxwebsoft.common.system.param;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 版本更新查询参数
*
* @author 科技小王子
* @since 2024-01-15 18:52:24
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "VersionParam对象", description = "版本更新查询参数")
public class VersionParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "ID")
@QueryField(type = QueryType.EQ)
private Integer id;
@Schema(description = "版本名")
private String versionName;
@Schema(description = "版本号")
@QueryField(type = QueryType.EQ)
private Integer versionCode;
@Schema(description = "下载链接")
private String androidDownloadUrl;
@Schema(description = "下载链接")
private String iosDownloadUrl;
@Schema(description = "更新日志")
private String updateInfo;
@Schema(description = "强制更新")
@QueryField(type = QueryType.EQ)
private Integer isHard;
@Schema(description = "热更")
@QueryField(type = QueryType.EQ)
private Integer isHot;
@Schema(description = "备注")
private String comments;
@Schema(description = "文章排序(数字越小越靠前)")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "状态, 0正常, 1冻结")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@QueryField(type = QueryType.EQ)
private Integer deleted;
}

View File

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

View File

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

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