Compare commits

...

259 Commits

Author SHA1 Message Date
ad23922a7c feat(entity): 添加接待人相关字段
- 新增receptionistId字段,记录接待人用户ID
- 新增receptionistName字段,记录接待人名称
- 字段均添加了相应的Schema描述信息feat(entity): 新增接待人用户ID及名称字段

- 在ShopDealerApply实体中添加receptionistId字段表示接待人用户ID
- 添加receptionistName字段用于存储接待人名称
- 对新增字段增加Swagger注解描述信息
- 保持原有房号和佣金比例字段不变
2026-04-09 13:59:31 +08:00
1a990087ac Merge remote-tracking branch 'origin/master' 2026-04-04 10:31:46 +08:00
13b4f626aa remove(AI): 移除所有AI相关模块代码
- 删除Ollama客户端及DTO类定义
- 移除AI聊天、分析、知识库控制器
- 清理AI相关数据传输对象
- 删除知识库实体类和映射器
- 移除AI分析服务和提示词模板
- 清理AI相关的XML映射文件
- 移除AI配置属性类定义
- 删除知识库服务实现类
2026-04-04 10:31:32 +08:00
84cd214277 feat(website): 新增应用发布管理功能
- 在CmsWebsite实体中添加发布状态、定价模式、应用描述等相关字段
- 实现应用上架审核流程:提交审核、撤回申请、下架、管理员审批等功能
- 添加审核列表分页查询接口,支持按发布状态筛选
- 更新路由配置,调整开发者资源路径
- 添加数据库表结构变更SQL脚本,增加发布管理相关字段
2026-04-01 01:04:49 +08:00
8a9d779d08 feat(website): 新增应用发布管理功能
- 在CmsWebsite实体中添加发布状态、定价模式、应用描述等相关字段
- 实现应用上架审核流程:提交审核、撤回申请、下架、管理员审批等功能
- 添加审核列表分页查询接口,支持按发布状态筛选
- 更新路由配置,调整开发者资源路径
- 添加数据库表结构变更SQL脚本,增加发布管理相关字段
2026-04-01 00:59:44 +08:00
f1dde97538 feat(website): 新增应用发布管理功能
- 在CmsWebsite实体中添加发布状态、定价模式、应用描述等相关字段
- 实现应用上架审核流程:提交审核、撤回申请、下架、管理员审批等功能
- 添加审核列表分页查询接口,支持按发布状态筛选
- 更新路由配置,调整开发者资源路径
- 添加数据库表结构变更SQL脚本,增加发布管理相关字段
2026-04-01 00:59:06 +08:00
5029be1f1f feat(ticket): 更新工单列表排序逻辑
- 实现工单状态优先级排序:pending > assigned > processing > resolved > closed
- 添加紧急程度优先级排序:urgent > high > normal > low
- 保持相同状态下按创建时间倒序排列
- 使用FIELD函数优化数据库查询排序性能
- 移除原有的简单排序规则并替换为复合排序逻辑
2026-04-01 00:35:07 +08:00
38ee4c65e6 feat(app): 添加开发者资源管理系统
- 创建app_resource数据表,支持服务器/数据库/云存储/域名/SSL证书等资源管理
- 实现AppResource实体类,包含各类资源的特定字段和通用字段
- 开发AppResourceController控制器,提供资源的增删改查和统计功能
- 实现AppResourceService服务层,包含业务逻辑和数据操作方法
- 创建AppResourceMapper数据访问层,支持分页查询和关联查询
- 添加AppResourceParam查询参数类,支持多条件筛选
- 集成MyBatis XML映射文件,实现复杂的关联查询和统计功能
- 实现基于用户权限的资源访问控制和逻辑删除机制
2026-03-31 19:59:24 +08:00
3ea8e652bd feat(app): 添加应用配置管理功能
- 创建应用配置表及实体类
- 实现应用配置的增删改查接口
- 添加配置值加密解密功能
- 支持按应用ID批量获取配置映射
- 实现配置的批量保存和删除功能
- 添加分页查询和列表查询支持
- 集成租户隔离和软删除功能
2026-03-30 19:42:53 +08:00
44e95a7273 feat(ticket): 使用忽略租户拦截器的方法获取用户信息
- 在工单创建时使用 userService.getByIdIgnoreTenant 替代 getById
- 在工单分配时使用 userService.getByIdIgnoreTenant 替代 getById
- 在回复创建时使用 userService.getByIdIgnoreTenant 替代 getById
- 添加注释说明使用 @InterceptorIgnore 绕过租户拦截器跨库查询
- 确保用户信息获取不受租户隔离限制影响
2026-03-30 14:31:52 +08:00
35fdc2dcfc feat(entity): 为工单实体添加附件字段的JSON序列化处理
- 在AppTicket和AppTicketReply实体中添加JsonArrayToStringDeserializer和JsonStringToArraySerializer注解
- 实现附件字段在数据库存储时从数组转换为JSON字符串
- 实现附件字段向前端响应时从JSON字符串转换为数组格式
- 添加JsonArrayToStringDeserializer类处理数组到字符串的反序列化
- 添加JsonStringToArraySerializer类处理字符串到数组的序列化
- 移除CMS网站映射器中的软删除条件过滤
2026-03-30 14:18:03 +08:00
875111d2d8 fix(mapper): 修复网站查询中的表别名引用问题
- 为app_user表添加别名au以避免字段冲突
- 确保website_id字段正确从别名表中引用
- 保持deleted字段的过滤条件一致性
2026-03-30 13:07:26 +08:00
ee2d95da91 feat(website): 添加网站查询中的协作成员筛选功能
- 在CmsWebsiteParam中新增memberUserId参数用于协作成员筛选
- 修改CmsWebsiteMapper.xml添加按memberUserId查询的SQL逻辑
- 实现通过app_user表关联查询指定用户的协作网站列表
- 支持同时查询用户直接创建和作为成员参与的网站
- 保持原有websiteIds和keywords查询条件的兼容性
2026-03-30 12:43:12 +08:00
85e0b062b8 feat(ticket): 实现工单系统并集成企业微信飞书通知
- 在控制器中统一返回格式,修复fail方法调用
- 实现工单提交、查询、回复等核心功能
- 添加工单状态管理(待处理、已分配、处理中、已解决、已关闭)
- 集成企业微信群机器人和飞书群机器人实时通知
- 实现异步推送机制支持四种通知场景:新工单、重新分配、新回复、状态变更
- 添加工单统计功能和用户权限控制
- 创建工单主表和回复表的数据库结构定义
2026-03-30 12:28:27 +08:00
a4d5c86134 feat(app): 添加应用工单系统功能
- 创建AppTicket实体类定义工单数据结构
- 实现AppTicketController提供工单CRUD接口
- 创建AppTicketReply实体支持工单回复功能
- 实现工单提交、查询、状态更新等核心业务逻辑
- 添加工单统计、分配、回复等功能接口
- 集成MyBatis Plus实现数据库操作
- 支持工单分类、优先级、状态流转管理
- 实现自动分配工单给技术成员机制
- 添加工单搜索、分页、权限控制功能
2026-03-30 11:59:41 +08:00
607589d2c5 feat(user): 添加根据手机号查询用户的跨租户功能
- 在 UserMapper 中新增 selectByPhone 方法,支持忽略租户隔离查询
- 更新 UserMapper.xml 中的 selectByPhone 查询语句,实现跨库查询逻辑
- 修改 UserServiceImpl 中的 getByPhone 方法,使用自定义 SQL 避免租户拦截器影响
- 实现跨租户查询,直接访问 gxwebsoft_core.sys_user 表获取用户信息
2026-03-30 11:39:23 +08:00
424929222f feat(contact): 添加联系表单销售线索功能支持
- 新增联系表单数据库表结构设计与实体类定义
- 实现联系表单提交接口,支持公开访问无需登录验证
- 添加后台管理接口,支持线索查询、状态更新和删除操作
- 集成企业微信和飞书机器人通知功能,实时推送新线索
- 在安全配置中开放联系表单提交接口访问权限
- 添加应用配置中的通知机器人Webhook配置项
2026-03-30 11:30:29 +08:00
df7a41f3c4 feat(contact): 添加联系表单销售线索功能支持
- 新增联系表单数据库表结构设计与实体类定义
- 实现联系表单提交接口,支持公开访问无需登录验证
- 添加后台管理接口,支持线索查询、状态更新和删除操作
- 集成企业微信和飞书机器人通知功能,实时推送新线索
- 在安全配置中开放联系表单提交接口访问权限
- 添加应用配置中的通知机器人Webhook配置项
2026-03-30 11:08:12 +08:00
bd2a92d832 feat(website): 完善应用类型管理和标识符唯一性校验
- 移除邀请用户接口的权限注解和操作日志注解
- 更新CmsWebsite实体类中的类型描述,支持多种应用类型
- 在保存和修改网站信息时增加websiteCode唯一性校验逻辑
- 实现create方法中根据应用类型自动设置管理后台地址和类型名称
- 添加generateUniqueCode方法确保websiteCode全局唯一
- 优化代码结构和业务逻辑处理
2026-03-29 02:36:07 +08:00
75c13bada8 refactor(app-user): 移除跨库JOIN依赖,改用冗余字段存储用户信息
- 在AppUser实体中新增nickname和phone冗余字段
- 移除AppUserMapper.xml中的跨库JOIN查询gxwebsoft_core.sys_user表
- 更新搜索条件使用本地冗余字段而非关联表字段
- 注入UserService用于写入冗余用户基础信息
- 邀请成员时查询系统用户信息并冗余存储到本地表
- 日志输出中增加用户名信息便于调试追踪
2026-03-29 01:19:48 +08:00
6aeface82d fix(database): 修复用户表关联查询的数据库模式问题
- 修改 AppUserMapper.xml 中的 LEFT JOIN 语句
- 为 sys_user 表添加 gxwebsoft_core 数据库前缀
- 确保跨数据库关联查询的正确性
- 保持与系统用户表的数据一致性
2026-03-29 00:52:46 +08:00
17352718c5 chore(config): 切换默认激活配置文件为开发环境
- 将 spring.profiles.active 从 ysb2 更改为 dev
- 更新默认运行环境配置以适应开发需求
2026-03-29 00:31:00 +08:00
7dede6f36f feat(app): 完善应用凭证、事件和用户管理功能
- 新增应用密钥凭证的创建、重置和状态管理功能
- 实现AppSecret自动生成功能并添加脱敏显示机制
- 增加应用操作动态的最新记录查询和批量清理功能
- 添加应用成员邀请和角色修改功能
- 优化查询条件支持精确匹配和租户隔离
- 集成网站信息关联查询并完善数据脱敏处理
2026-03-28 21:47:32 +08:00
35cc034af1 feat(generator): 添加代码生成工具及应用实体模块
- 新增 AppGenerator 代码生成工具类,支持多平台模板生成
- 生成 app_credential 应用密钥凭证的完整CRUD功能模块
- 生成 app_event 应用操作动态的完整CRUD功能模块
- 添加对应的 Entity、Controller、Service、Mapper 和 Param 类
- 配置多平台模板支持(Vue、UniApp、移动端页面)
- 实现 app.config.ts 自动更新功能
- 优化后端实现指南文档说明
2026-03-28 21:33:42 +08:00
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
557 changed files with 37883 additions and 11599 deletions

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ ehthumbs.db
Thumbs.db
/file/
/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

@@ -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,459 @@
# 客户跟进7步骤后端实现指南
## 📋 概述
本指南详细说明如何实现客户跟进7个步骤功能的后端代码包括数据库设计、Java后端实现和API接口。
## 🗄️ 数据库设计
### 1. 修改 credit_mp_customer 表结构
```sql
-- 为第5-7步添加字段第1-4步基础字段已存在如未包含审核时间/审核人字段,请先补齐 step1-4 的 approved_at/approved_by
-- 参考docs/sql/credit_mp_customer_step1_4_approval_columns.sql
-- 第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. **性能优化**:大量数据时考虑分页和索引优化
## 🔄 后续扩展
可以考虑的功能:
- 跟进模板和标准化流程
- 自动提醒和通知
- 数据统计和报表
- 跟进效率分析
- 客户满意度评估

19
docs/app_config.sql Normal file
View File

@@ -0,0 +1,19 @@
-- 应用配置表
CREATE TABLE app_config (
config_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '配置ID',
website_id INT NOT NULL COMMENT '应用ID',
config_key VARCHAR(100) NOT NULL COMMENT '配置键',
config_value TEXT NOT NULL COMMENT '配置值JSON或字符串',
config_type VARCHAR(50) NOT NULL DEFAULT 'general' COMMENT '配置类型general/api/callback/wechat/payment/git等',
is_encrypted TINYINT DEFAULT 0 COMMENT '是否加密 0否 1是',
is_secret TINYINT DEFAULT 0 COMMENT '是否敏感信息 0否 1是',
description VARCHAR(500) DEFAULT '' COMMENT '配置说明',
sort_number INT DEFAULT 0 COMMENT '排序号',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户id',
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT DEFAULT 0 COMMENT '是否删除 0否 1是',
UNIQUE KEY uk_website_key (website_id, config_key, deleted),
INDEX idx_website_type (website_id, config_type),
INDEX idx_tenant (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用配置表';

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,48 @@
-- 工单主表
CREATE TABLE IF NOT EXISTS `app_ticket` (
`ticket_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '工单ID',
`ticket_no` VARCHAR(32) NOT NULL COMMENT '工单编号TK-yyyyMMddHHmmss+4位随机',
`title` VARCHAR(200) NOT NULL COMMENT '工单标题',
`content` TEXT NOT NULL COMMENT '工单内容描述',
`website_id` BIGINT DEFAULT NULL COMMENT '关联应用ID',
`website_name` VARCHAR(100) DEFAULT NULL COMMENT '应用名称(冗余)',
`category` VARCHAR(30) NOT NULL DEFAULT 'other' COMMENT '分类: bug/feature/consultation/complaint/other',
`priority` VARCHAR(20) NOT NULL DEFAULT 'normal' COMMENT '优先级: low/normal/high/urgent',
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/assigned/processing/resolved/closed/rejected',
`attachments` TEXT DEFAULT NULL COMMENT '附件JSON数组',
`submit_user_id` INT NOT NULL COMMENT '提交人用户ID',
`submit_user_name` VARCHAR(50) DEFAULT NULL COMMENT '提交人昵称(冗余)',
`submit_user_avatar` VARCHAR(500) DEFAULT NULL COMMENT '提交人头像(冗余)',
`assignee_id` INT DEFAULT NULL COMMENT '处理人用户ID',
`assignee_name` VARCHAR(50) DEFAULT NULL COMMENT '处理人昵称(冗余)',
`assignee_avatar` VARCHAR(500) DEFAULT NULL COMMENT '处理人头像(冗余)',
`reply_count` INT NOT NULL DEFAULT 0 COMMENT '回复数量',
`resolved_time` DATETIME DEFAULT NULL COMMENT '解决时间',
`closed_time` DATETIME DEFAULT NULL COMMENT '关闭时间',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`ticket_id`),
UNIQUE KEY `uk_ticket_no` (`ticket_no`),
KEY `idx_submit_user` (`submit_user_id`),
KEY `idx_assignee` (`assignee_id`),
KEY `idx_website_status` (`website_id`, `status`),
KEY `idx_status_create` (`status`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用工单';
-- 工单回复表
CREATE TABLE IF NOT EXISTS `app_ticket_reply` (
`reply_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '回复ID',
`ticket_id` BIGINT NOT NULL COMMENT '关联工单ID',
`content` TEXT NOT NULL COMMENT '回复内容',
`attachments` TEXT DEFAULT NULL COMMENT '附件JSON数组',
`user_id` INT NOT NULL COMMENT '回复人用户ID',
`user_name` VARCHAR(50) DEFAULT NULL COMMENT '回复人昵称(冗余)',
`user_avatar` VARCHAR(500) DEFAULT NULL COMMENT '回复人头像(冗余)',
`is_staff` TINYINT NOT NULL DEFAULT 0 COMMENT '是否技术人员/客服: 0否 1是',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`reply_id`),
KEY `idx_ticket_id` (`ticket_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工单回复';

View File

@@ -0,0 +1,24 @@
-- 修复:审核接口(updateStepApproval)会写入 follow_step{1..4}_approved_at / follow_step{1..4}_approved_by
-- 若数据库缺少这些列会触发Unknown column 'follow_step1_approved_at' in 'field list'
--
-- 建议先检查:
-- SHOW COLUMNS FROM credit_mp_customer LIKE 'follow_step%_approved%';
--
-- 然后按需执行下面的 ALTER TABLE如果你的 MySQL 版本支持,也可以改为 ADD COLUMN IF NOT EXISTS
-- 第1步案件受理
ALTER TABLE credit_mp_customer ADD COLUMN follow_step1_approved_at VARCHAR(255) NULL COMMENT '第1步审核时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step1_approved_by BIGINT NULL COMMENT '第1步审核人ID';
-- 第2步材料准备
ALTER TABLE credit_mp_customer ADD COLUMN follow_step2_approved_at VARCHAR(255) NULL COMMENT '第2步审核时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step2_approved_by BIGINT NULL COMMENT '第2步审核人ID';
-- 第3步案件办理
ALTER TABLE credit_mp_customer ADD COLUMN follow_step3_approved_at VARCHAR(255) NULL COMMENT '第3步审核时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step3_approved_by BIGINT NULL COMMENT '第3步审核人ID';
-- 第4步送达签收
ALTER TABLE credit_mp_customer ADD COLUMN follow_step4_approved_at VARCHAR(255) NULL COMMENT '第4步审核时间';
ALTER TABLE credit_mp_customer ADD COLUMN follow_step4_approved_by BIGINT NULL COMMENT '第4步审核人ID';

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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- spring-boot-web -->
<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,129 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.entity.AppConfig;
import com.gxwebsoft.app.param.AppConfigParam;
import com.gxwebsoft.app.service.AppConfigService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 应用配置表 Controller
*/
@Slf4j
@Tag(name = "应用配置管理")
@RestController
@RequestMapping("/api/app/app-config")
public class AppConfigController extends BaseController {
@Resource
private AppConfigService appConfigService;
/**
* 分页查询应用配置
*/
@Operation(summary = "分页查询应用配置")
@GetMapping("/page")
public ApiResult<PageResult<AppConfig>> page(AppConfigParam param) {
return success(new PageResult<>(appConfigService.page(param)));
}
/**
* 获取应用配置列表
*/
@Operation(summary = "获取应用配置列表")
@GetMapping()
public ApiResult<List<AppConfig>> list(AppConfigParam param) {
return success(appConfigService.list(param));
}
/**
* 根据应用ID获取配置映射
*/
@Operation(summary = "根据应用ID获取配置映射")
@GetMapping("/map/{websiteId}")
public ApiResult<Map<String, Object>> getConfigsByWebsiteId(@PathVariable Integer websiteId) {
return success(appConfigService.getConfigsByWebsiteId(websiteId));
}
/**
* 获取单个配置值
*/
@Operation(summary = "获取单个配置值")
@GetMapping("/value")
public ApiResult<String> getConfigValue(@RequestParam Integer websiteId, @RequestParam String configKey) {
return success(appConfigService.getConfigValue(websiteId, configKey),null);
}
/**
* 保存配置
*/
@Operation(summary = "保存配置")
@PostMapping()
public ApiResult<?> save(@RequestBody AppConfig config) {
appConfigService.saveConfig(config);
return success("保存成功");
}
/**
* 批量保存配置
*/
@Operation(summary = "批量保存配置")
@PostMapping("/batch")
public ApiResult<?> batchSave(@RequestBody BatchSaveRequest request) {
appConfigService.batchSaveConfig(request.getWebsiteId(), request.getConfigs());
return success("保存成功");
}
/**
* 更新配置
*/
@Operation(summary = "更新配置")
@PutMapping()
public ApiResult<?> update(@RequestBody AppConfig config) {
appConfigService.updateConfig(config);
return success("更新成功");
}
/**
* 删除配置
*/
@Operation(summary = "删除配置")
@DeleteMapping("/{configId}")
public ApiResult<?> delete(@PathVariable Integer configId) {
appConfigService.deleteConfig(configId);
return success("删除成功");
}
/**
* 批量保存请求对象
*/
public static class BatchSaveRequest {
private Integer websiteId;
private List<AppConfig> configs;
public Integer getWebsiteId() {
return websiteId;
}
public void setWebsiteId(Integer websiteId) {
this.websiteId = websiteId;
}
public List<AppConfig> getConfigs() {
return configs;
}
public void setConfigs(List<AppConfig> configs) {
this.configs = configs;
}
}
}

View File

@@ -0,0 +1,151 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.service.AppCredentialService;
import com.gxwebsoft.app.entity.AppCredential;
import com.gxwebsoft.app.param.AppCredentialParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 应用密钥凭证控制器
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Tag(name = "应用密钥凭证管理")
@RestController
@RequestMapping("/api/app/app-credential")
public class AppCredentialController extends BaseController {
@Resource
private AppCredentialService appCredentialService;
@Operation(summary = "分页查询应用密钥凭证")
@GetMapping("/page")
public ApiResult<PageResult<AppCredential>> page(AppCredentialParam param) {
return success(appCredentialService.pageRel(param));
}
@Operation(summary = "查询全部应用密钥凭证")
@GetMapping()
public ApiResult<List<AppCredential>> list(AppCredentialParam param) {
return success(appCredentialService.listRel(param));
}
@Operation(summary = "根据id查询应用密钥凭证")
@GetMapping("/{id}")
public ApiResult<AppCredential> get(@PathVariable("id") Integer id) {
return success(appCredentialService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('app:appCredential:save')")
@OperationLog
@Operation(summary = "创建应用密钥凭证(自动生成 AppID 和 AppSecret")
@PostMapping()
public ApiResult<?> save(@RequestBody AppCredential appCredential) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
appCredential.setUserId(loginUser.getUserId());
// 创建并生成密钥
AppCredential result = appCredentialService.createCredential(appCredential);
return success("创建成功,请保存 AppSecret该信息仅展示一次", result);
}
@PreAuthorize("hasAuthority('app:appCredential:update')")
@OperationLog
@Operation(summary = "修改应用密钥凭证(名称/类型/备注等,不含密钥)")
@PutMapping()
public ApiResult<?> update(@RequestBody AppCredential appCredential) {
// 防止通过此接口直接修改 appId/appSecret
appCredential.setAppId(null);
appCredential.setAppSecret(null);
if (appCredentialService.updateById(appCredential)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appCredential:update')")
@OperationLog
@Operation(summary = "重置 AppSecret重新生成密钥")
@PostMapping("/resetSecret/{id}")
public ApiResult<?> resetSecret(@PathVariable("id") Long id) {
try {
AppCredential result = appCredentialService.resetSecret(id);
return success("重置成功,请保存新 AppSecret该信息仅展示一次", result);
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
@PreAuthorize("hasAuthority('app:appCredential:update')")
@OperationLog
@Operation(summary = "禁用/启用凭证")
@PutMapping("/status/{id}/{status}")
public ApiResult<?> updateStatus(@PathVariable("id") Long id, @PathVariable("status") Integer status) {
if (appCredentialService.updateStatus(id, status)) {
return success(status == 0 ? "已启用" : "已禁用");
}
return fail("操作失败");
}
@PreAuthorize("hasAuthority('app:appCredential:remove')")
@OperationLog
@Operation(summary = "删除应用密钥凭证")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (appCredentialService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('app:appCredential:save')")
@OperationLog
@Operation(summary = "批量添加应用密钥凭证")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<AppCredential> list) {
if (appCredentialService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('app:appCredential:update')")
@OperationLog
@Operation(summary = "批量修改应用密钥凭证")
@PutMapping("/batch")
public ApiResult<?> updateBatch(@RequestBody BatchParam<AppCredential> batchParam) {
if (batchParam.update(appCredentialService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appCredential:remove')")
@OperationLog
@Operation(summary = "批量删除应用密钥凭证")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (appCredentialService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -0,0 +1,133 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.service.AppEventService;
import com.gxwebsoft.app.entity.AppEvent;
import com.gxwebsoft.app.param.AppEventParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 应用操作动态控制器
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Tag(name = "应用操作动态管理")
@RestController
@RequestMapping("/api/app/app-event")
public class AppEventController extends BaseController {
@Resource
private AppEventService appEventService;
@Operation(summary = "分页查询操作动态")
@GetMapping("/page")
public ApiResult<PageResult<AppEvent>> page(AppEventParam param) {
return success(appEventService.pageRel(param));
}
@Operation(summary = "查询全部操作动态")
@GetMapping()
public ApiResult<List<AppEvent>> list(AppEventParam param) {
return success(appEventService.listRel(param));
}
@Operation(summary = "根据id查询操作动态")
@GetMapping("/{id}")
public ApiResult<AppEvent> get(@PathVariable("id") Integer id) {
return success(appEventService.getByIdRel(id));
}
@Operation(summary = "获取应用最新一条动态(用于卡片展示)")
@GetMapping("/latest/{websiteId}")
public ApiResult<AppEvent> getLatest(@PathVariable("websiteId") Long websiteId) {
return success(appEventService.getLatestEvent(websiteId));
}
@PreAuthorize("hasAuthority('app:appEvent:save')")
@OperationLog
@Operation(summary = "手动记录操作动态")
@PostMapping()
public ApiResult<?> save(@RequestBody AppEvent appEvent) {
User loginUser = getLoginUser();
if (loginUser != null) {
appEvent.setUserId(loginUser.getUserId());
appEvent.setTenantId(loginUser.getTenantId());
if (appEvent.getOperatorId() == null) {
appEvent.setOperatorId(loginUser.getUserId().longValue());
}
if (appEvent.getOperator() == null) {
appEvent.setOperator(loginUser.getNickname());
}
}
if (appEventService.save(appEvent)) {
return success("记录成功");
}
return fail("记录失败");
}
@PreAuthorize("hasAuthority('app:appEvent:update')")
@OperationLog
@Operation(summary = "修改操作动态")
@PutMapping()
public ApiResult<?> update(@RequestBody AppEvent appEvent) {
if (appEventService.updateById(appEvent)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appEvent:remove')")
@OperationLog
@Operation(summary = "删除操作动态")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (appEventService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('app:appEvent:remove')")
@OperationLog
@Operation(summary = "清空应用所有动态记录")
@DeleteMapping("/clear/{websiteId}")
public ApiResult<?> clearByWebsiteId(@PathVariable("websiteId") Long websiteId) {
// 只清空当前租户下的数据
AppEventParam param = new AppEventParam();
param.setWebsiteId(websiteId);
List<AppEvent> list = appEventService.listRel(param);
if (list.isEmpty()) {
return success("暂无动态记录");
}
List<Long> ids = list.stream().map(AppEvent::getId).collect(java.util.stream.Collectors.toList());
if (appEventService.removeByIds(ids)) {
return success("已清空 " + ids.size() + " 条动态记录");
}
return fail("清空失败");
}
@PreAuthorize("hasAuthority('app:appEvent:remove')")
@OperationLog
@Operation(summary = "批量删除操作动态")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (appEventService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -0,0 +1,145 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppResource;
import com.gxwebsoft.app.param.AppResourceParam;
import com.gxwebsoft.app.service.AppResourceService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 开发者资源管理控制器(服务器/数据库/云存储/域名/SSL
*
* @author 科技小王子
* @since 2026-03-31
*/
@Slf4j
@Tag(name = "开发者资源管理")
@RestController
@RequestMapping("/api/app/developer-resource")
public class AppResourceController extends BaseController {
@Resource
private AppResourceService appResourceService;
// ─── 查询接口 ─────────────────────────────────────────────────
@Operation(summary = "分页查询资源列表")
@GetMapping("/page")
public ApiResult<PageResult<AppResource>> page(AppResourceParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
// 普通开发者只能查自己的资源
param.setUserId(loginUser.getUserId());
param.setTenantId(loginUser.getTenantId());
return success(appResourceService.pageRel(param));
}
@Operation(summary = "查询资源列表(不分页)")
@GetMapping
public ApiResult<List<AppResource>> list(AppResourceParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
param.setUserId(loginUser.getUserId());
param.setTenantId(loginUser.getTenantId());
return success(appResourceService.listRel(param));
}
@Operation(summary = "获取资源详情")
@GetMapping("/{resourceId}")
public ApiResult<AppResource> get(@PathVariable Long resourceId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
AppResource resource = appResourceService.getByIdRel(resourceId);
if (resource == null) return fail("资源不存在", null);
if (!resource.getUserId().equals(loginUser.getUserId())) return fail("无权访问此资源", null);
return success(resource);
}
@Operation(summary = "统计各类型资源数量")
@GetMapping("/stats")
public ApiResult<Map<String, Long>> stats() {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
return success(appResourceService.countByType(loginUser.getUserId(), loginUser.getTenantId()));
}
// ─── 新增/修改接口 ────────────────────────────────────────────
@OperationLog
@Operation(summary = "新增资源")
@PostMapping
public ApiResult<AppResource> save(@RequestBody AppResource resource) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
if (resource.getResourceType() == null || resource.getResourceType().isEmpty()) {
return fail("资源类型不能为空", null);
}
if (resource.getName() == null || resource.getName().isEmpty()) {
return fail("资源名称不能为空", null);
}
resource.setTenantId(loginUser.getTenantId());
try {
AppResource result = appResourceService.addResource(resource, loginUser.getUserId());
return success("添加成功", result);
} catch (Exception e) {
return fail(e.getMessage(), null);
}
}
@OperationLog
@Operation(summary = "修改资源")
@PutMapping
public ApiResult<AppResource> update(@RequestBody AppResource resource) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录", null);
try {
AppResource result = appResourceService.updateResource(resource);
return success("修改成功", result);
} catch (Exception e) {
return fail(e.getMessage(), null);
}
}
// ─── 删除接口 ─────────────────────────────────────────────────
@OperationLog
@Operation(summary = "删除资源(逻辑删除)")
@DeleteMapping("/{resourceId}")
public ApiResult<?> remove(@PathVariable Long resourceId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
try {
appResourceService.removeResource(resourceId, loginUser.getUserId());
return success("删除成功");
} catch (Exception e) {
return fail(e.getMessage());
}
}
@OperationLog
@Operation(summary = "批量删除资源")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Long> ids) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
try {
for (Long id : ids) {
appResourceService.removeResource(id, loginUser.getUserId());
}
return success("批量删除成功");
} catch (Exception e) {
return fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,150 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.app.entity.AppTicket;
import com.gxwebsoft.app.entity.AppTicketReply;
import com.gxwebsoft.app.param.AppTicketParam;
import com.gxwebsoft.app.service.AppTicketService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 应用工单控制器
*
* @author 科技小王子
* @since 2026-03-30
*/
@Slf4j
@Tag(name = "应用工单管理")
@RestController
@RequestMapping("/api/app/ticket")
public class AppTicketController extends BaseController {
@Resource
private AppTicketService appTicketService;
// ─── 客户端接口 ────────────────────────────────────────────────
@Operation(summary = "查询我的工单(分页)")
@GetMapping("/my")
public ApiResult<PageResult<AppTicket>> myTickets(AppTicketParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录",null);
return success(appTicketService.myPage(param, loginUser.getUserId()));
}
@Operation(summary = "提交工单")
@PostMapping("/submit")
public ApiResult<AppTicket> submit(@RequestBody AppTicket ticket) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录",null);
try {
AppTicket result = appTicketService.submit(ticket, loginUser.getUserId());
return success("工单提交成功", result);
} catch (Exception e) {
return fail(e.getMessage(),null);
}
}
@Operation(summary = "关闭工单(提交人)")
@PutMapping("/{ticketId}/close")
public ApiResult<?> close(@PathVariable Long ticketId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
try {
appTicketService.closeByUser(ticketId, loginUser.getUserId());
return success("工单已关闭");
} catch (Exception e) {
return fail(e.getMessage());
}
}
// ─── 技术端接口 ────────────────────────────────────────────────
@Operation(summary = "查询所有工单(技术人员)")
@GetMapping("/list")
public ApiResult<PageResult<AppTicket>> allTickets(AppTicketParam param) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录",null);
return success(appTicketService.allPage(param));
}
@Operation(summary = "获取工单详情")
@GetMapping("/{ticketId}")
public ApiResult<AppTicket> detail(@PathVariable Long ticketId) {
return success(appTicketService.getById(ticketId));
}
@Operation(summary = "更新工单状态(技术人员)")
@PutMapping("/status")
public ApiResult<?> updateStatus(@RequestBody Map<String, Object> body) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
Long ticketId = Long.valueOf(body.get("ticketId").toString());
String status = body.get("status").toString();
appTicketService.updateStatus(ticketId, status, loginUser.getUserId());
return success("状态已更新");
}
@Operation(summary = "分配处理人(管理员)")
@PutMapping("/assign")
public ApiResult<?> assign(@RequestBody Map<String, Object> body) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录");
Long ticketId = Long.valueOf(body.get("ticketId").toString());
Integer assigneeId = Integer.valueOf(body.get("assigneeId").toString());
appTicketService.assign(ticketId, assigneeId);
return success("分配成功");
}
// ─── 回复接口 ─────────────────────────────────────────────────
@Operation(summary = "获取工单回复列表")
@GetMapping("/{ticketId}/replies")
public ApiResult<List<AppTicketReply>> replies(@PathVariable Long ticketId) {
return success(appTicketService.getReplies(ticketId));
}
@Operation(summary = "提交工单回复")
@PostMapping("/reply")
public ApiResult<AppTicketReply> reply(@RequestBody AppTicketReply reply) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录",null);
if (reply.getContent() == null || reply.getContent().trim().isEmpty()) {
return fail("回复内容不能为空",null);
}
try {
AppTicketReply result = appTicketService.addReply(reply, loginUser.getUserId());
return success("回复成功", result);
} catch (Exception e) {
return fail(e.getMessage(),null);
}
}
// ─── 统计 & 辅助 ─────────────────────────────────────────────
@Operation(summary = "工单统计数据")
@GetMapping("/stats")
public ApiResult<Map<String, Long>> stats(
@RequestParam(required = false) Long websiteId) {
User loginUser = getLoginUser();
if (loginUser == null) return fail("请先登录",null);
// 技术端不限制用户维度;客户端通过路由区分
return success(appTicketService.stats(websiteId, null));
}
@Operation(summary = "获取技术人员列表(用于分配)")
@GetMapping("/staff-list")
public ApiResult<List<Map<String, Object>>> staffList() {
return success(appTicketService.getTechStaffList());
}
}

View File

@@ -0,0 +1,167 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.service.AppUserService;
import com.gxwebsoft.app.entity.AppUser;
import com.gxwebsoft.app.param.AppUserParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 应用成员控制器
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Tag(name = "应用成员管理")
@RestController
@RequestMapping("/api/app/app-user")
public class AppUserController extends BaseController {
@Resource
private AppUserService appUserService;
@Operation(summary = "分页查询应用成员")
@GetMapping("/page")
public ApiResult<PageResult<AppUser>> page(AppUserParam param) {
return success(appUserService.pageRel(param));
}
@Operation(summary = "查询全部应用成员")
@GetMapping()
public ApiResult<List<AppUser>> list(AppUserParam param) {
return success(appUserService.listRel(param));
}
@Operation(summary = "根据id查询应用成员")
@GetMapping("/{id}")
public ApiResult<AppUser> get(@PathVariable("id") Integer id) {
return success(appUserService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('app:appUser:save')")
@OperationLog
@Operation(summary = "添加应用成员(手动添加)")
@PostMapping()
public ApiResult<?> save(@RequestBody AppUser appUser) {
User loginUser = getLoginUser();
if (loginUser != null) {
appUser.setUserId(loginUser.getUserId());
appUser.setTenantId(loginUser.getTenantId());
}
if (appUserService.save(appUser)) {
return success("添加成功");
}
return fail("添加失败");
}
@Operation(summary = "邀请用户成为应用成员支持用户ID或手机号")
@PostMapping("/invite")
public ApiResult<?> invite(@RequestBody AppUser appUser) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
// 支持手机号邀请:若 userId 为空但传了 phone则先按手机号查出用户
if (appUser.getUserId() == null && appUser.getPhone() != null && !appUser.getPhone().isEmpty()) {
User targetUser = appUserService.findUserByPhone(appUser.getPhone());
if (targetUser == null) {
return fail("手机号未注册,请确认后再试");
}
appUser.setUserId(targetUser.getUserId());
}
if (appUser.getUserId() == null) {
return fail("请输入用户ID或手机号");
}
try {
AppUser result = appUserService.inviteUser(
appUser.getWebsiteId(),
appUser.getUserId(),
appUser.getRole(),
loginUser.getUserId()
);
return success("邀请成功", result);
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
@PreAuthorize("hasAuthority('app:appUser:update')")
@OperationLog
@Operation(summary = "修改应用成员信息")
@PutMapping()
public ApiResult<?> update(@RequestBody AppUser appUser) {
if (appUserService.updateById(appUser)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appUser:update')")
@OperationLog
@Operation(summary = "修改成员角色")
@PutMapping("/role/{id}/{role}")
public ApiResult<?> updateRole(@PathVariable("id") Long id, @PathVariable("role") String role) {
if (appUserService.updateRole(id, role)) {
return success("角色修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appUser:remove')")
@OperationLog
@Operation(summary = "移除应用成员")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (appUserService.removeById(id)) {
return success("已移除");
}
return fail("移除失败");
}
@PreAuthorize("hasAuthority('app:appUser:save')")
@OperationLog
@Operation(summary = "批量添加应用成员")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<AppUser> list) {
if (appUserService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('app:appUser:update')")
@OperationLog
@Operation(summary = "批量修改应用成员")
@PutMapping("/batch")
public ApiResult<?> updateBatch(@RequestBody BatchParam<AppUser> batchParam) {
if (batchParam.update(appUserService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appUser:remove')")
@OperationLog
@Operation(summary = "批量移除应用成员")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (appUserService.removeByIds(ids)) {
return success("移除成功");
}
return fail("移除失败");
}
}

View File

@@ -0,0 +1,173 @@
package com.gxwebsoft.app.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.app.service.AppVersionService;
import com.gxwebsoft.app.entity.AppVersion;
import com.gxwebsoft.app.param.AppVersionParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 应用版本发布记录控制器
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Tag(name = "应用版本发布管理")
@RestController
@RequestMapping("/api/app/app-version")
public class AppVersionController extends BaseController {
@Resource
private AppVersionService appVersionService;
@Operation(summary = "分页查询版本记录")
@GetMapping("/page")
public ApiResult<PageResult<AppVersion>> page(AppVersionParam param) {
return success(appVersionService.pageRel(param));
}
@Operation(summary = "查询全部版本记录")
@GetMapping()
public ApiResult<List<AppVersion>> list(AppVersionParam param) {
return success(appVersionService.listRel(param));
}
@Operation(summary = "根据id查询版本")
@GetMapping("/{id}")
public ApiResult<AppVersion> get(@PathVariable("id") Integer id) {
return success(appVersionService.getByIdRel(id));
}
@Operation(summary = "获取应用当前版本")
@GetMapping("/current/{websiteId}")
public ApiResult<AppVersion> getCurrentVersion(@PathVariable("websiteId") Long websiteId) {
return success(appVersionService.getCurrentVersion(websiteId));
}
@PreAuthorize("hasAuthority('app:appVersion:save')")
@OperationLog
@Operation(summary = "新增版本(构建中状态)")
@PostMapping()
public ApiResult<?> save(@RequestBody AppVersion appVersion) {
User loginUser = getLoginUser();
if (loginUser != null) {
appVersion.setUserId(loginUser.getUserId());
appVersion.setTenantId(loginUser.getTenantId());
}
// 默认为构建中状态
if (appVersion.getStatus() == null) {
appVersion.setStatus(0);
}
if (appVersion.getEnv() == null) {
appVersion.setEnv("production");
}
appVersion.setIsCurrent(false);
if (appVersionService.save(appVersion)) {
return success("创建成功");
}
return fail("创建失败");
}
@PreAuthorize("hasAuthority('app:appVersion:update')")
@OperationLog
@Operation(summary = "修改版本信息")
@PutMapping()
public ApiResult<?> update(@RequestBody AppVersion appVersion) {
if (appVersionService.updateById(appVersion)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appVersion:update')")
@OperationLog
@Operation(summary = "发布版本(将此版本设为当前运行版本)")
@PostMapping("/publish/{id}")
public ApiResult<?> publish(@PathVariable("id") Long id) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
try {
appVersionService.publish(id, loginUser.getUserId());
return success("发布成功");
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
@PreAuthorize("hasAuthority('app:appVersion:update')")
@OperationLog
@Operation(summary = "回滚到指定版本")
@PostMapping("/rollback/{id}")
public ApiResult<?> rollback(@PathVariable("id") Long id) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
try {
appVersionService.rollback(id, loginUser.getUserId());
return success("回滚成功");
} catch (RuntimeException e) {
return fail(e.getMessage());
}
}
@PreAuthorize("hasAuthority('app:appVersion:remove')")
@OperationLog
@Operation(summary = "删除版本记录")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (appVersionService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('app:appVersion:save')")
@OperationLog
@Operation(summary = "批量添加版本")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<AppVersion> list) {
if (appVersionService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('app:appVersion:update')")
@OperationLog
@Operation(summary = "批量修改版本")
@PutMapping("/batch")
public ApiResult<?> updateBatch(@RequestBody BatchParam<AppVersion> batchParam) {
if (batchParam.update(appVersionService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('app:appVersion:remove')")
@OperationLog
@Operation(summary = "批量删除版本记录")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (appVersionService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -0,0 +1,87 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 应用配置表
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_config")
public class AppConfig implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 配置ID
*/
@TableId(value = "config_id", type = IdType.AUTO)
private Integer configId;
/**
* 应用ID
*/
private Integer websiteId;
/**
* 配置键
*/
private String configKey;
/**
* 配置值JSON或字符串
*/
private String configValue;
/**
* 配置类型general/api/callback/wechat/payment/git等
*/
private String configType;
/**
* 是否加密 0否 1是
*/
private Integer isEncrypted;
/**
* 是否敏感信息 0否 1是
*/
private Integer isSecret;
/**
* 配置说明
*/
private String description;
/**
* 排序号
*/
private Integer sortNumber;
/**
* 租户id
*/
private Long tenantId;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Long createdTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updatedTime;
/**
* 是否删除 0否 1是
*/
@TableLogic
private Integer deleted;
}

View File

@@ -1,70 +1,57 @@
package com.gxwebsoft.credit.entity;
package com.gxwebsoft.app.entity;
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 com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 被执行人
* 应用密钥凭证
*
* @author 科技小王子
* @since 2026-01-12 08:10:43
* @since 2026-03-28 21:29:43
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "CreditJudgmentDebtorHistory对象", description = "被执行人")
public class CreditJudgmentDebtorHistory implements Serializable {
@Schema(name = "AppCredential对象", description = "应用密钥凭证")
public class AppCredential implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "ID")
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private Long id;
@Schema(description = "案号")
private String caseNumber;
@Schema(description = "关联应用ID")
private Long websiteId;
@Schema(description = "被执行人名称")
@Schema(description = "凭证名称,如生产环境密钥")
private String name;
@Schema(description = "被执行人")
private String name1;
@Schema(description = "App ID公开")
private String appId;
@Schema(description = "证件号/组织机构代码")
private String code;
@Schema(description = "App Secret加密存储")
private String appSecret;
@Schema(description = "链接")
private String url;
@Schema(description = "凭证类型: server/client/webhook")
private String type;
@Schema(description = "是否多企业")
private Integer type;
@Schema(description = "权限范围,空格分隔")
private String scopes;
@Schema(description = "立案日")
private String occurrenceTime;
@Schema(description = "到期时间NULL=永不过")
private LocalDateTime expireTime;
@Schema(description = "执行标的(元)")
private String amount;
@Schema(description = "最后使用时间")
private LocalDateTime lastUsedAt;
@Schema(description = "法院")
private String courtName;
@Schema(description = "数据状态")
private String dataStatus;
@Schema(description = "企业ID")
private Integer companyId;
@Schema(description = "备注")
private String comments;
@Schema(description = "是否推荐")
private Integer recommend;
private String remark;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;

View File

@@ -0,0 +1,76 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用操作动态
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppEvent对象", description = "应用操作动态")
public class AppEvent implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "关联应用ID")
private Long websiteId;
@Schema(description = "事件类型: created/published/updated/domain_bound/member_added/status_changed")
private String eventType;
@Schema(description = "事件标题,如已发布")
private String title;
@Schema(description = "详细描述")
private String content;
@Schema(description = "操作人用户ID")
private Long operatorId;
@Schema(description = "操作人名称(冗余)")
private String operator;
@Schema(description = "关联ID如版本ID")
private Long refId;
@Schema(description = "关联类型")
private String refType;
@Schema(description = "扩展数据")
private String extra;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "状态, 0正常, 1冻结")
private Integer status;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,121 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 开发者资源(服务器/数据库/云存储/域名/SSL证书
*
* @author 科技小王子
* @since 2026-03-31
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_resource")
@Schema(name = "AppResource对象", description = "开发者资源")
public class AppResource implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "资源ID")
@TableId(value = "resource_id", type = IdType.AUTO)
private Long resourceId;
@Schema(description = "资源类型: server/database/storage/domain/ssl")
private String resourceType;
@Schema(description = "资源名称")
private String name;
@Schema(description = "服务商: tencent/aliyun/huawei/other")
private String provider;
@Schema(description = "关联应用ID可选")
private Long websiteId;
@Schema(description = "关联应用名称(冗余)")
private String websiteName;
// ─── 服务器字段 ───────────────────────────────────────
@Schema(description = "IP地址服务器用")
private String ip;
// ─── 数据库字段 ───────────────────────────────────────
@Schema(description = "数据库类型: MySQL/PostgreSQL/Redis/MongoDB数据库用")
private String dbType;
@Schema(description = "连接主机地址(数据库用)")
private String host;
@Schema(description = "连接端口(数据库用)")
private Integer port;
// ─── 云存储字段 ───────────────────────────────────────
@Schema(description = "地区/Region云存储用")
private String region;
@Schema(description = "访问权限: public-read/private云存储用")
private String acl;
@Schema(description = "已用空间(字节,云存储用)")
private Long usedBytes;
// ─── 域名字段 ─────────────────────────────────────────
@Schema(description = "域名(域名用)")
private String domain;
@Schema(description = "注册商(域名用)")
private String registrar;
@Schema(description = "是否已备案(域名用)")
private Boolean icp;
@Schema(description = "ICP备案号域名用")
private String icpNo;
@Schema(description = "是否已绑定SSL域名用冗余")
private Boolean sslBound;
// ─── SSL证书字段 ──────────────────────────────────────
@Schema(description = "证书类型: DV/OV/EVSSL用")
private String certType;
@Schema(description = "颁发机构SSL用")
private String issuer;
// ─── 通用字段 ─────────────────────────────────────────
@Schema(description = "状态: running/stopped/expired/pending")
private String status;
@Schema(description = "到期时间")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate expireAt;
@Schema(description = "备注")
private String remark;
@Schema(description = "所属用户ID")
private Integer userId;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "是否删除: 0否 1是")
private Integer deleted;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,103 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.gxwebsoft.common.core.config.JsonArrayToStringDeserializer;
import com.gxwebsoft.common.core.config.JsonStringToArraySerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 应用工单
*
* @author 科技小王子
* @since 2026-03-30
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_ticket")
@Schema(name = "AppTicket对象", description = "应用工单")
public class AppTicket implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "工单ID")
@TableId(value = "ticket_id", type = IdType.AUTO)
private Long ticketId;
@Schema(description = "工单编号TK-yyyyMMddHHmmss+4位随机")
private String ticketNo;
@Schema(description = "工单标题")
private String title;
@Schema(description = "工单内容描述")
private String content;
@Schema(description = "关联应用ID")
private Long websiteId;
@Schema(description = "应用名称(冗余)")
private String websiteName;
@Schema(description = "工单分类: bug/feature/consultation/complaint/other")
private String category;
@Schema(description = "优先级: low/normal/high/urgent")
private String priority;
@Schema(description = "状态: pending/assigned/processing/resolved/closed/rejected")
private String status;
@Schema(description = "附件JSON数组")
@JsonDeserialize(using = JsonArrayToStringDeserializer.class)
@JsonSerialize(using = JsonStringToArraySerializer.class)
private String attachments;
@Schema(description = "提交人用户ID")
private Integer submitUserId;
@Schema(description = "提交人昵称(冗余)")
private String submitUserName;
@Schema(description = "提交人头像(冗余)")
private String submitUserAvatar;
@Schema(description = "分配的处理人用户ID")
private Integer assigneeId;
@Schema(description = "处理人昵称(冗余)")
private String assigneeName;
@Schema(description = "处理人头像(冗余)")
private String assigneeAvatar;
@Schema(description = "回复数量")
private Integer replyCount;
@Schema(description = "解决时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime resolvedTime;
@Schema(description = "关闭时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime closedTime;
@Schema(description = "是否删除: 0否 1是")
private Integer deleted;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,64 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.gxwebsoft.common.core.config.JsonArrayToStringDeserializer;
import com.gxwebsoft.common.core.config.JsonStringToArraySerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 工单回复
*
* @author 科技小王子
* @since 2026-03-30
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("app_ticket_reply")
@Schema(name = "AppTicketReply对象", description = "工单回复")
public class AppTicketReply implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "回复ID")
@TableId(value = "reply_id", type = IdType.AUTO)
private Long replyId;
@Schema(description = "关联工单ID")
private Long ticketId;
@Schema(description = "回复内容")
private String content;
@Schema(description = "附件JSON数组")
@JsonDeserialize(using = JsonArrayToStringDeserializer.class)
@JsonSerialize(using = JsonStringToArraySerializer.class)
private String attachments;
@Schema(description = "回复人用户ID")
private Integer userId;
@Schema(description = "回复人昵称(冗余)")
private String userName;
@Schema(description = "回复人头像(冗余)")
private String userAvatar;
@Schema(description = "是否是技术人员/客服: 0否 1是")
private Integer isStaff;
@Schema(description = "是否删除: 0否 1是")
private Integer deleted;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,73 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用成员
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppUser对象", description = "应用成员")
public class AppUser implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "关联应用ID")
private Long websiteId;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "用户名(冗余)")
private String username;
@Schema(description = "昵称(冗余)")
private String nickname;
@Schema(description = "头像(冗余)")
private String avatar;
@Schema(description = "手机号(冗余,脱敏存储)")
private String phone;
@Schema(description = "角色: owner/admin/developer/viewer")
private String role;
@Schema(description = "邀请人用户ID")
private Long inviteBy;
@Schema(description = "加入时间")
private LocalDateTime inviteTime;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "状态, 0正常, 1冻结")
private Integer status;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,85 @@
package com.gxwebsoft.app.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用版本发布记录
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "AppVersion对象", description = "应用版本发布记录")
public class AppVersion implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "关联应用ID")
private Long websiteId;
@Schema(description = "版本号,如 1.0.0")
private String versionNo;
@Schema(description = "版本名称")
private String versionName;
@Schema(description = "版本更新说明")
private String changelog;
@Schema(description = "安装包地址")
private String packageUrl;
@Schema(description = "包大小(字节)")
private Long packageSize;
@Schema(description = "包MD5/SHA256")
private String packageHash;
@Schema(description = "环境: development/staging/production")
private String env;
@Schema(description = "状态 0=构建中 1=已发布 2=已回滚 3=构建失败")
private Integer status;
@Schema(description = "是否为当前版本")
private Boolean isCurrent;
@Schema(description = "发布人用户ID")
private Long publishBy;
@Schema(description = "发布时间")
private LocalDateTime publishTime;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,43 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppConfig;
import com.gxwebsoft.app.param.AppConfigParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* 应用配置表 Mapper
*
* @author 科技小王子
*/
@Mapper
public interface AppConfigMapper extends BaseMapper<AppConfig> {
/**
* 批量获取应用配置
*/
List<Map<String, Object>> selectConfigsByWebsiteId(Integer websiteId);
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<AppConfig>
*/
List<AppConfig> selectPageRel(@Param("page") IPage<AppConfig> page,
@Param("param") AppConfigParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<AppConfig>
*/
List<AppConfig> selectListRel(@Param("param") AppConfigParam param);
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppCredential;
import com.gxwebsoft.app.param.AppCredentialParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 应用密钥凭证Mapper
*
* @author 科技小王子
* @since 2026-03-28 21:29:43
*/
public interface AppCredentialMapper extends BaseMapper<AppCredential> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<AppCredential>
*/
List<AppCredential> selectPageRel(@Param("page") IPage<AppCredential> page,
@Param("param") AppCredentialParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<AppCredential> selectListRel(@Param("param") AppCredentialParam param);
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppEvent;
import com.gxwebsoft.app.param.AppEventParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 应用操作动态Mapper
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
public interface AppEventMapper extends BaseMapper<AppEvent> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<AppEvent>
*/
List<AppEvent> selectPageRel(@Param("page") IPage<AppEvent> page,
@Param("param") AppEventParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<AppEvent> selectListRel(@Param("param") AppEventParam param);
}

View File

@@ -0,0 +1,35 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppResource;
import com.gxwebsoft.app.param.AppResourceParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 开发者资源 Mapper
*
* @author 科技小王子
* @since 2026-03-31
*/
public interface AppResourceMapper extends BaseMapper<AppResource> {
/**
* 分页查询(关联应用名称)
*/
List<AppResource> selectPageRel(@Param("page") IPage<AppResource> page,
@Param("param") AppResourceParam param);
/**
* 查询全部列表(关联应用名称)
*/
List<AppResource> selectListRel(@Param("param") AppResourceParam param);
/**
* 统计各类型资源数量,返回 [{resourceType, cnt}]
*/
List<java.util.Map<String, Object>> countByType(@Param("userId") Integer userId,
@Param("tenantId") Integer tenantId);
}

View File

@@ -0,0 +1,11 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppTicket;
import org.apache.ibatis.annotations.Param;
/**
* 应用工单 Mapper
*/
public interface AppTicketMapper extends BaseMapper<AppTicket> {
}

View File

@@ -0,0 +1,10 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.entity.AppTicketReply;
/**
* 工单回复 Mapper
*/
public interface AppTicketReplyMapper extends BaseMapper<AppTicketReply> {
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppUser;
import com.gxwebsoft.app.param.AppUserParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 应用成员Mapper
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
public interface AppUserMapper extends BaseMapper<AppUser> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<AppUser>
*/
List<AppUser> selectPageRel(@Param("page") IPage<AppUser> page,
@Param("param") AppUserParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<AppUser> selectListRel(@Param("param") AppUserParam param);
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.app.entity.AppVersion;
import com.gxwebsoft.app.param.AppVersionParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 应用版本发布记录Mapper
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
public interface AppVersionMapper extends BaseMapper<AppVersion> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<AppVersion>
*/
List<AppVersion> selectPageRel(@Param("page") IPage<AppVersion> page,
@Param("param") AppVersionParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<AppVersion> selectListRel(@Param("param") AppVersionParam param);
}

View File

@@ -0,0 +1,66 @@
<?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.app.mapper.AppConfigMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*
FROM app_config a
<where>
a.deleted = 0
<if test="param.configId != null">
AND a.config_id = #{param.configId}
</if>
<if test="param.websiteId != null">
AND a.website_id = #{param.websiteId}
</if>
<if test="param.configKey != null and param.configKey != ''">
AND a.config_key LIKE CONCAT('%', #{param.configKey}, '%')
</if>
<if test="param.configType != null and param.configType != ''">
AND a.config_type = #{param.configType}
</if>
<if test="param.isSecret != null">
AND a.is_secret = #{param.isSecret}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</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 param.keywords != ''">
AND a.config_key LIKE CONCAT('%', #{param.keywords}, '%')
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppConfig">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppConfig">
<include refid="selectSql"></include>
</select>
<!-- 批量获取应用配置(自动解密) -->
<select id="selectConfigsByWebsiteId" resultType="java.util.HashMap">
SELECT
config_key as configKey,
config_value as configValue,
config_type as configType,
is_encrypted as isEncrypted,
is_secret as isSecret,
description
FROM app_config
WHERE website_id = #{websiteId}
AND deleted = 0
ORDER BY config_type, sort_number, config_id
</select>
</mapper>

View File

@@ -0,0 +1,71 @@
<?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.app.mapper.AppCredentialMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*, w.website_name, w.website_code, w.website_icon
FROM app_credential a
LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.websiteId != null">
AND a.website_id = #{param.websiteId}
</if>
<if test="param.name != null and param.name != ''">
AND a.name LIKE CONCAT('%', #{param.name}, '%')
</if>
<if test="param.appId != null and param.appId != ''">
AND a.app_id = #{param.appId}
</if>
<if test="param.type != null and param.type != ''">
AND a.type = #{param.type}
</if>
<if test="param.scopes != null and param.scopes != ''">
AND a.scopes LIKE CONCAT('%', #{param.scopes}, '%')
</if>
<if test="param.remark != null and param.remark != ''">
AND a.remark LIKE CONCAT('%', #{param.remark}, '%')
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.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 param.keywords != ''">
AND (a.name LIKE CONCAT('%', #{param.keywords}, '%')
OR a.app_id LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppCredential">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppCredential">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -0,0 +1,66 @@
<?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.app.mapper.AppEventMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*, w.website_name, w.website_code, w.website_icon
FROM app_event a
LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.websiteId != null">
AND a.website_id = #{param.websiteId}
</if>
<if test="param.eventType != null and param.eventType != ''">
AND a.event_type = #{param.eventType}
</if>
<if test="param.title != null and param.title != ''">
AND a.title LIKE CONCAT('%', #{param.title}, '%')
</if>
<if test="param.operatorId != null">
AND a.operator_id = #{param.operatorId}
</if>
<if test="param.refId != null">
AND a.ref_id = #{param.refId}
</if>
<if test="param.refType != null and param.refType != ''">
AND a.ref_type = #{param.refType}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</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 param.keywords != ''">
AND (a.title LIKE CONCAT('%', #{param.keywords}, '%')
OR a.content LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
ORDER BY a.create_time DESC
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppEvent">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppEvent">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -0,0 +1,75 @@
<?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.app.mapper.AppResourceMapper">
<!-- 关联查询 SQL -->
<sql id="selectSql">
SELECT a.*, w.website_name
FROM app_resource a
LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0
<where>
a.deleted = 0
<if test="param.resourceId != null">
AND a.resource_id = #{param.resourceId}
</if>
<if test="param.resourceType != null and param.resourceType != ''">
AND a.resource_type = #{param.resourceType}
</if>
<if test="param.websiteId != null">
AND a.website_id = #{param.websiteId}
</if>
<if test="param.provider != null and param.provider != ''">
AND a.provider = #{param.provider}
</if>
<if test="param.status != null and param.status != ''">
AND a.status = #{param.status}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
<if test="param.keywords != null and param.keywords != ''">
AND (
a.name LIKE CONCAT('%', #{param.keywords}, '%')
OR a.ip LIKE CONCAT('%', #{param.keywords}, '%')
OR a.domain LIKE CONCAT('%', #{param.keywords}, '%')
OR a.host LIKE CONCAT('%', #{param.keywords}, '%')
)
</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>
</where>
ORDER BY a.create_time DESC
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppResource">
<include refid="selectSql"/>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppResource">
<include refid="selectSql"/>
</select>
<!-- 按类型统计数量 -->
<select id="countByType" resultType="java.util.Map">
SELECT resource_type AS resourceType, COUNT(*) AS cnt
FROM app_resource
WHERE deleted = 0
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="tenantId != null">
AND tenant_id = #{tenantId}
</if>
GROUP BY resource_type
</select>
</mapper>

View File

@@ -0,0 +1,62 @@
<?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.app.mapper.AppUserMapper">
<!-- 关联查询sql已移除跨库 JOIN gxwebsoft_core.sys_user用户信息从冗余字段读取 -->
<sql id="selectSql">
SELECT a.*, w.website_name, w.website_code, w.website_icon
FROM app_user a
LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.websiteId != null">
AND a.website_id = #{param.websiteId}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.username != null and param.username != ''">
AND (a.username LIKE CONCAT('%', #{param.username}, '%')
OR a.nickname LIKE CONCAT('%', #{param.username}, '%'))
</if>
<if test="param.role != null and param.role != ''">
AND a.role = #{param.role}
</if>
<if test="param.inviteBy != null">
AND a.invite_by = #{param.inviteBy}
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</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 param.keywords != ''">
AND (a.username LIKE CONCAT('%', #{param.keywords}, '%')
OR a.nickname LIKE CONCAT('%', #{param.keywords}, '%'))
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppUser">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppUser">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -0,0 +1,66 @@
<?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.app.mapper.AppVersionMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*, w.website_name, w.website_code, w.website_icon
FROM app_version a
LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.websiteId != null">
AND a.website_id = #{param.websiteId}
</if>
<if test="param.versionNo != null and param.versionNo != ''">
AND a.version_no = #{param.versionNo}
</if>
<if test="param.versionName != null and param.versionName != ''">
AND a.version_name LIKE CONCAT('%', #{param.versionName}, '%')
</if>
<if test="param.env != null and param.env != ''">
AND a.env = #{param.env}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.isCurrent != null">
AND a.is_current = #{param.isCurrent}
</if>
<if test="param.publishBy != null">
AND a.publish_by = #{param.publishBy}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</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 param.keywords != ''">
AND (a.version_no LIKE CONCAT('%', #{param.keywords}, '%')
OR a.version_name LIKE CONCAT('%', #{param.keywords}, '%')
OR a.changelog LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.app.entity.AppVersion">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.app.entity.AppVersion">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -0,0 +1,38 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.web.BaseParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用配置表查询参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class AppConfigParam extends BaseParam {
/**
* 配置ID
*/
private Integer configId;
/**
* 应用ID
*/
private Integer websiteId;
/**
* 配置键
*/
private String configKey;
/**
* 配置类型
*/
private String configType;
/**
* 是否敏感信息
*/
private Integer isSecret;
}

View File

@@ -1,6 +1,5 @@
package com.gxwebsoft.dormitory.param;
package com.gxwebsoft.app.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;
@@ -10,60 +9,57 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 宿舍床位查询参数
* 应用密钥凭证查询参数
*
* @author 科技小王子
* @since 2025-10-03 10:49:27
* @since 2026-03-28 21:29:43
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "宿舍床位查询参数")
public class DormitoryBedParam extends BaseParam {
@Schema(name = "AppCredentialParam对象", description = "应用密钥凭证查询参数")
public class AppCredentialParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "ID")
@Schema(description = "自增ID")
@QueryField(type = QueryType.EQ)
private Integer id;
private Long id;
@Schema(description = "宿舍名称")
@Schema(description = "关联应用ID")
@QueryField(type = QueryType.EQ)
private Long websiteId;
@Schema(description = "凭证名称,如生产环境密钥")
private String name;
@Schema(description = "编号")
private String code;
@Schema(description = "楼栋ID")
@Schema(description = "App ID公开")
@QueryField(type = QueryType.EQ)
private Integer buildingId;
private String appId;
@Schema(description = "楼层ID")
@Schema(description = "凭证类型: server/client/webhook")
@QueryField(type = QueryType.EQ)
private Integer floorId;
private String type;
@Schema(description = "宿舍ID")
@QueryField(type = QueryType.EQ)
private Integer recordId;
@Schema(description = "权限范围,空格分隔")
private String scopes;
@Schema(description = "学生ID")
private Integer userId;
@Schema(description = "上下铺 1下铺 2上铺 0无")
@QueryField(type = QueryType.EQ)
private Boolean bunk;
@Schema(description = "充电口")
@QueryField(type = QueryType.EQ)
private Boolean chargingPort;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序(数字越小越靠前)")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@Schema(description = "备注")
private String comments;
@Schema(description = "状态, 0正常, 1报修")
@Schema(description = "状态, 0正常, 1冻结")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@QueryField(type = QueryType.EQ)
private Integer deleted;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
}

View File

@@ -0,0 +1,63 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用操作动态查询参数
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "AppEventParam对象", description = "应用操作动态查询参数")
public class AppEventParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@QueryField(type = QueryType.EQ)
private Long id;
@Schema(description = "关联应用ID")
@QueryField(type = QueryType.EQ)
private Long websiteId;
@Schema(description = "事件类型: created/published/updated/domain_bound/member_added/status_changed")
@QueryField(type = QueryType.EQ)
private String eventType;
@Schema(description = "事件标题(模糊)")
private String title;
@Schema(description = "操作人用户ID")
@QueryField(type = QueryType.EQ)
private Long operatorId;
@Schema(description = "关联ID如版本ID")
@QueryField(type = QueryType.EQ)
private Long refId;
@Schema(description = "关联类型")
@QueryField(type = QueryType.EQ)
private String refType;
@Schema(description = "状态, 0正常, 1冻结")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "租户ID")
@QueryField(type = QueryType.EQ)
private Integer tenantId;
}

View File

@@ -0,0 +1,54 @@
package com.gxwebsoft.app.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 2026-03-31
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "AppResourceParam对象", description = "开发者资源查询参数")
public class AppResourceParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "资源ID")
@QueryField(type = QueryType.EQ)
private Long resourceId;
@Schema(description = "资源类型: server/database/storage/domain/ssl")
@QueryField(type = QueryType.EQ)
private String resourceType;
@Schema(description = "关联应用ID")
@QueryField(type = QueryType.EQ)
private Long websiteId;
@Schema(description = "服务商")
@QueryField(type = QueryType.EQ)
private String provider;
@Schema(description = "状态")
@QueryField(type = QueryType.EQ)
private String status;
@Schema(description = "所属用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "租户ID")
@QueryField(type = QueryType.EQ)
private Integer tenantId;
@Schema(description = "关键词(名称/IP/域名/Host模糊搜索")
private String keywords;
}

View File

@@ -0,0 +1,33 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.web.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 工单查询参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "工单查询参数")
public class AppTicketParam extends PageParam {
@Schema(description = "关联应用ID")
private Long websiteId;
@Schema(description = "工单状态")
private String status;
@Schema(description = "工单分类")
private String category;
@Schema(description = "优先级")
private String priority;
@Schema(description = "处理人ID传0=未分配)")
private Integer assigneeId;
@Schema(description = "关键词(标题/工单编号)")
private String keywords;
}

View File

@@ -0,0 +1,59 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用成员查询参数
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "AppUserParam对象", description = "应用成员查询参数")
public class AppUserParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@QueryField(type = QueryType.EQ)
private Long id;
@Schema(description = "关联应用ID")
@QueryField(type = QueryType.EQ)
private Long websiteId;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "用户名(模糊搜索)")
private String username;
@Schema(description = "角色: owner/admin/developer/viewer")
@QueryField(type = QueryType.EQ)
private String role;
@Schema(description = "邀请人用户ID")
@QueryField(type = QueryType.EQ)
private Long inviteBy;
@Schema(description = "排序(数字越小越靠前)")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@Schema(description = "状态, 0正常, 1冻结")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "租户ID")
@QueryField(type = QueryType.EQ)
private Integer tenantId;
}

View File

@@ -0,0 +1,63 @@
package com.gxwebsoft.app.param;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应用版本发布记录查询参数
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "AppVersionParam对象", description = "应用版本发布记录查询参数")
public class AppVersionParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@QueryField(type = QueryType.EQ)
private Long id;
@Schema(description = "关联应用ID")
@QueryField(type = QueryType.EQ)
private Long websiteId;
@Schema(description = "版本号,如 1.0.0")
@QueryField(type = QueryType.EQ)
private String versionNo;
@Schema(description = "版本名称(模糊)")
private String versionName;
@Schema(description = "环境: development/staging/production")
@QueryField(type = QueryType.EQ)
private String env;
@Schema(description = "状态 0=构建中 1=已发布 2=已回滚 3=构建失败")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "是否为当前版本")
@QueryField(type = QueryType.EQ)
private Boolean isCurrent;
@Schema(description = "发布人用户ID")
@QueryField(type = QueryType.EQ)
private Long publishBy;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "租户ID")
@QueryField(type = QueryType.EQ)
private Integer tenantId;
}

View File

@@ -0,0 +1,58 @@
package com.gxwebsoft.app.service;
import com.gxwebsoft.app.entity.AppConfig;
import com.gxwebsoft.app.param.AppConfigParam;
import java.util.List;
import java.util.Map;
/**
* 应用配置表 Service
*/
public interface AppConfigService {
/**
* 分页查询应用配置
*/
List<AppConfig> page(AppConfigParam param);
/**
* 获取应用配置列表
*/
List<AppConfig> list(AppConfigParam param);
/**
* 根据应用ID获取配置映射自动解密
*/
Map<String, Object> getConfigsByWebsiteId(Integer websiteId);
/**
* 获取单个配置值
*/
String getConfigValue(Integer websiteId, String configKey);
/**
* 保存配置
*/
void saveConfig(AppConfig config);
/**
* 批量保存配置
*/
void batchSaveConfig(Integer websiteId, List<AppConfig> configs);
/**
* 更新配置
*/
void updateConfig(AppConfig config);
/**
* 删除配置
*/
void deleteConfig(Integer configId);
/**
* 根据应用ID删除所有配置
*/
void deleteByWebsiteId(Integer websiteId);
}

View File

@@ -0,0 +1,57 @@
package com.gxwebsoft.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.app.entity.AppCredential;
import com.gxwebsoft.app.param.AppCredentialParam;
import java.util.List;
/**
* 应用密钥凭证Service
*
* @author 科技小王子
* @since 2026-03-28 21:29:43
*/
public interface AppCredentialService extends IService<AppCredential> {
/**
* 分页关联查询
*/
PageResult<AppCredential> pageRel(AppCredentialParam param);
/**
* 关联查询全部
*/
List<AppCredential> listRel(AppCredentialParam param);
/**
* 根据id查询
*/
AppCredential getByIdRel(Integer id);
/**
* 创建凭证(自动生成 appId 和 appSecret
*
* @param credential 凭证基础信息
* @return 包含明文 appSecret 的凭证对象(仅此次返回,之后不再展示)
*/
AppCredential createCredential(AppCredential credential);
/**
* 重置密钥(重新生成 appSecret
*
* @param id 凭证ID
* @return 包含新明文 appSecret 的凭证对象
*/
AppCredential resetSecret(Long id);
/**
* 禁用/启用凭证
*
* @param id 凭证ID
* @param status 0=正常 1=冻结
*/
boolean updateStatus(Long id, Integer status);
}

View File

@@ -0,0 +1,60 @@
package com.gxwebsoft.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.app.entity.AppEvent;
import com.gxwebsoft.app.param.AppEventParam;
import java.util.List;
/**
* 应用操作动态Service
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
public interface AppEventService extends IService<AppEvent> {
/**
* 分页关联查询
*/
PageResult<AppEvent> pageRel(AppEventParam param);
/**
* 关联查询全部
*/
List<AppEvent> listRel(AppEventParam param);
/**
* 根据id查询
*/
AppEvent getByIdRel(Integer id);
/**
* 记录应用事件(便捷方法,供其他模块调用)
*
* @param websiteId 应用ID
* @param eventType 事件类型created/published/updated/domain_bound/member_added/status_changed 等)
* @param title 事件标题
* @param content 详细描述
* @param operatorId 操作人用户ID
* @param operatorName 操作人名称
* @param refId 关联记录ID如版本ID
* @param refType 关联记录类型
*/
void logEvent(Long websiteId, String eventType, String title, String content,
Long operatorId, String operatorName, Long refId, String refType);
/**
* 快捷记录事件(无关联记录)
*/
void logEvent(Long websiteId, String eventType, String title, Long operatorId, String operatorName);
/**
* 获取指定应用的最新一条事件(用于卡片"最新动态"展示)
*
* @param websiteId 应用ID
*/
AppEvent getLatestEvent(Long websiteId);
}

View File

@@ -0,0 +1,39 @@
package com.gxwebsoft.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.app.entity.AppResource;
import com.gxwebsoft.app.param.AppResourceParam;
import com.gxwebsoft.common.core.web.PageResult;
import java.util.List;
import java.util.Map;
/**
* 开发者资源 Service
*
* @author 科技小王子
* @since 2026-03-31
*/
public interface AppResourceService extends IService<AppResource> {
/** 分页查询(关联应用名称) */
PageResult<AppResource> pageRel(AppResourceParam param);
/** 列表查询 */
List<AppResource> listRel(AppResourceParam param);
/** 详情(关联查询) */
AppResource getByIdRel(Long resourceId);
/** 新增资源 */
AppResource addResource(AppResource resource, Integer userId);
/** 更新资源 */
AppResource updateResource(AppResource resource);
/** 删除资源(逻辑删除) */
void removeResource(Long resourceId, Integer userId);
/** 按资源类型统计数量 [{resourceType, cnt}] */
Map<String, Long> countByType(Integer userId, Integer tenantId);
}

View File

@@ -0,0 +1,46 @@
package com.gxwebsoft.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.app.entity.AppTicket;
import com.gxwebsoft.app.entity.AppTicketReply;
import com.gxwebsoft.app.param.AppTicketParam;
import com.gxwebsoft.common.core.web.PageResult;
import java.util.List;
import java.util.Map;
/**
* 应用工单 Service
*/
public interface AppTicketService extends IService<AppTicket> {
/** 客户端:查自己提交的工单(分页) */
PageResult<AppTicket> myPage(AppTicketParam param, Integer userId);
/** 技术端:查询所有工单(分页) */
PageResult<AppTicket> allPage(AppTicketParam param);
/** 提交工单(自动分配) */
AppTicket submit(AppTicket ticket, Integer userId);
/** 更新状态(技术人员操作) */
void updateStatus(Long ticketId, String status, Integer operatorId);
/** 分配处理人 */
void assign(Long ticketId, Integer assigneeId);
/** 关闭工单(提交人操作) */
void closeByUser(Long ticketId, Integer userId);
/** 获取工单回复列表 */
List<AppTicketReply> getReplies(Long ticketId);
/** 添加回复 */
AppTicketReply addReply(AppTicketReply reply, Integer userId);
/** 统计数据 */
Map<String, Long> stats(Long websiteId, Integer userId);
/** 获取技术人员列表角色developer/admin 的应用成员) */
List<Map<String, Object>> getTechStaffList();
}

View File

@@ -0,0 +1,68 @@
package com.gxwebsoft.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.app.entity.AppUser;
import com.gxwebsoft.app.param.AppUserParam;
import java.util.List;
/**
* 应用成员Service
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
public interface AppUserService extends IService<AppUser> {
/**
* 分页关联查询
*/
PageResult<AppUser> pageRel(AppUserParam param);
/**
* 关联查询全部
*/
List<AppUser> listRel(AppUserParam param);
/**
* 根据id查询
*/
AppUser getByIdRel(Integer id);
/**
* 邀请成员加入应用
*
* @param websiteId 应用ID
* @param userId 被邀请的用户ID
* @param role 分配的角色
* @param inviteBy 邀请人用户ID
* @return 成员记录
*/
AppUser inviteUser(Long websiteId, Integer userId, String role, Integer inviteBy);
/**
* 修改成员角色
*
* @param id 成员记录ID
* @param role 新角色
*/
boolean updateRole(Long id, String role);
/**
* 检查用户是否已是应用成员
*
* @param websiteId 应用ID
* @param userId 用户ID
*/
boolean isMember(Long websiteId, Integer userId);
/**
* 根据手机号查找系统用户(用于手机号邀请)
*
* @param phone 手机号
* @return 系统用户,不存在返回 null
*/
com.gxwebsoft.common.system.entity.User findUserByPhone(String phone);
}

View File

@@ -0,0 +1,56 @@
package com.gxwebsoft.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.app.entity.AppVersion;
import com.gxwebsoft.app.param.AppVersionParam;
import java.util.List;
/**
* 应用版本发布记录Service
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
public interface AppVersionService extends IService<AppVersion> {
/**
* 分页关联查询
*/
PageResult<AppVersion> pageRel(AppVersionParam param);
/**
* 关联查询全部
*/
List<AppVersion> listRel(AppVersionParam param);
/**
* 根据id查询
*/
AppVersion getByIdRel(Integer id);
/**
* 发布版本(将此版本设为当前版本,其他版本 isCurrent=false
*
* @param id 版本ID
* @param publishBy 发布人用户ID
*/
boolean publish(Long id, Integer publishBy);
/**
* 回滚到指定版本(将此版本设为当前版本,当前版本标为已回滚)
*
* @param id 要回滚到的版本ID
* @param publishBy 操作人用户ID
*/
boolean rollback(Long id, Integer publishBy);
/**
* 获取指定应用的当前版本
*
* @param websiteId 应用ID
*/
AppVersion getCurrentVersion(Long websiteId);
}

View File

@@ -0,0 +1,270 @@
package com.gxwebsoft.app.service.impl;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.app.entity.AppConfig;
import com.gxwebsoft.app.mapper.AppConfigMapper;
import com.gxwebsoft.app.param.AppConfigParam;
import com.gxwebsoft.app.service.AppConfigService;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 应用配置表 Service 实现类
*
* @author 科技小王子
*/
@Slf4j
@Service
public class AppConfigServiceImpl extends ServiceImpl<AppConfigMapper, AppConfig> implements AppConfigService {
@Resource
private UserService userService;
/**
* 配置加密密钥(从配置文件读取)
*/
@Value("${app.config.encrypt-key:GXWebsoft2024!@#$}")
private String encryptKey;
/**
* 分页查询应用配置
*/
@Override
public List<AppConfig> page(AppConfigParam param) {
PageParam<AppConfig, AppConfigParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, config_id asc");
List<AppConfig> list = baseMapper.selectPageRel(page, param);
return page.sortRecords(list);
}
/**
* 分页查询应用配置返回PageResult
*/
public PageResult<AppConfig> pageRel(AppConfigParam param) {
PageParam<AppConfig, AppConfigParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, config_id asc");
List<AppConfig> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
/**
* 获取应用配置列表
*/
@Override
public List<AppConfig> list(AppConfigParam param) {
List<AppConfig> list = baseMapper.selectListRel(param);
PageParam<AppConfig, AppConfigParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, config_id asc");
return page.sortRecords(list);
}
/**
* 根据应用ID获取配置映射自动解密
*/
@Override
public Map<String, Object> getConfigsByWebsiteId(Integer websiteId) {
List<Map<String, Object>> configs = baseMapper.selectConfigsByWebsiteId(websiteId);
Map<String, Object> result = new HashMap<>();
for (Map<String, Object> config : configs) {
String configKey = (String) config.get("configKey");
Object configValue = config.get("configValue");
Integer isEncrypted = (Integer) config.get("isEncrypted");
// 解密
if (isEncrypted != null && isEncrypted == 1 && configValue != null) {
try {
configValue = decrypt((String) configValue);
} catch (Exception e) {
log.error("配置解密失败: {}", configKey, e);
}
}
result.put(configKey, configValue);
}
return result;
}
/**
* 获取单个配置值
*/
@Override
public String getConfigValue(Integer websiteId, String configKey) {
LambdaQueryWrapper<AppConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AppConfig::getWebsiteId, websiteId);
wrapper.eq(AppConfig::getConfigKey, configKey);
AppConfig config = getOne(wrapper);
if (config == null || config.getConfigValue() == null) {
return null;
}
// 解密
if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1) {
return decrypt(config.getConfigValue());
}
return config.getConfigValue();
}
/**
* 保存配置
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void saveConfig(AppConfig config) {
// 设置租户ID
Integer tenantId = getCurrentTenantId();
if (tenantId != null) {
config.setTenantId(Long.valueOf(tenantId));
}
// 加密敏感信息
if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1 && config.getConfigValue() != null) {
config.setConfigValue(encrypt(config.getConfigValue()));
}
save(config);
}
/**
* 批量保存配置
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void batchSaveConfig(Integer websiteId, List<AppConfig> configs) {
// 设置租户ID
Integer tenantId = getCurrentTenantId();
if (tenantId != null) {
for (AppConfig config : configs) {
config.setTenantId(Long.valueOf(tenantId));
}
}
// 先删除该应用的所有配置
remove(new LambdaQueryWrapper<AppConfig>()
.eq(AppConfig::getWebsiteId, websiteId));
// 批量插入新配置
for (AppConfig config : configs) {
config.setWebsiteId(websiteId);
// 加密敏感信息
if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1 && config.getConfigValue() != null) {
config.setConfigValue(encrypt(config.getConfigValue()));
}
save(config);
}
}
/**
* 更新配置
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void updateConfig(AppConfig config) {
// 加密敏感信息
if (config.getIsEncrypted() != null && config.getIsEncrypted() == 1 && config.getConfigValue() != null) {
config.setConfigValue(encrypt(config.getConfigValue()));
}
updateById(config);
}
/**
* 删除配置
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteConfig(Integer configId) {
removeById(configId);
}
/**
* 根据应用ID删除所有配置
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteByWebsiteId(Integer websiteId) {
remove(new LambdaQueryWrapper<AppConfig>()
.eq(AppConfig::getWebsiteId, websiteId));
}
/**
* 构建查询条件
*/
private LambdaQueryWrapper<AppConfig> buildQueryWrapper(AppConfigParam param) {
LambdaQueryWrapper<AppConfig> wrapper = new LambdaQueryWrapper<>();
if (param.getConfigId() != null) {
wrapper.eq(AppConfig::getConfigId, param.getConfigId());
}
if (param.getWebsiteId() != null) {
wrapper.eq(AppConfig::getWebsiteId, param.getWebsiteId());
}
if (param.getConfigKey() != null) {
wrapper.like(AppConfig::getConfigKey, param.getConfigKey());
}
if (param.getConfigType() != null) {
wrapper.eq(AppConfig::getConfigType, param.getConfigType());
}
if (param.getIsSecret() != null) {
wrapper.eq(AppConfig::getIsSecret, param.getIsSecret());
}
wrapper.orderByAsc(AppConfig::getConfigType)
.orderByAsc(AppConfig::getSortNumber)
.orderByAsc(AppConfig::getConfigId);
return wrapper;
}
/**
* AES 加密
*/
private String encrypt(String plainText) {
AES aes = SecureUtil.aes(encryptKey.getBytes());
return aes.encryptBase64(plainText);
}
/**
* AES 解密
*/
private String decrypt(String cipherText) {
AES aes = SecureUtil.aes(encryptKey.getBytes());
return aes.decryptStr(cipherText);
}
/**
* 获取当前登录用户的租户ID
*/
private Integer getCurrentTenantId() {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof User) {
return ((User) authentication.getPrincipal()).getTenantId();
}
} catch (Exception e) {
log.error("获取当前用户租户ID失败", e);
}
return null;
}
}

View File

@@ -0,0 +1,125 @@
package com.gxwebsoft.app.service.impl;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.app.mapper.AppCredentialMapper;
import com.gxwebsoft.app.service.AppCredentialService;
import com.gxwebsoft.app.entity.AppCredential;
import com.gxwebsoft.app.param.AppCredentialParam;
import com.gxwebsoft.common.core.utils.CommonUtil;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 应用密钥凭证Service实现
*
* @author 科技小王子
* @since 2026-03-28 21:29:43
*/
@Slf4j
@Service
public class AppCredentialServiceImpl extends ServiceImpl<AppCredentialMapper, AppCredential> implements AppCredentialService {
@Override
public PageResult<AppCredential> pageRel(AppCredentialParam param) {
PageParam<AppCredential, AppCredentialParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<AppCredential> list = baseMapper.selectPageRel(page, param);
// 脱敏处理appSecret 不对外展示(仅创建/重置时返回明文)
list.forEach(this::maskSecret);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<AppCredential> listRel(AppCredentialParam param) {
List<AppCredential> list = baseMapper.selectListRel(param);
PageParam<AppCredential, AppCredentialParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time desc");
list = page.sortRecords(list);
list.forEach(this::maskSecret);
return list;
}
@Override
public AppCredential getByIdRel(Integer id) {
AppCredentialParam param = new AppCredentialParam();
param.setId(id.longValue());
AppCredential credential = param.getOne(baseMapper.selectListRel(param));
if (credential != null) {
maskSecret(credential);
}
return credential;
}
@Override
public AppCredential createCredential(AppCredential credential) {
// 生成 AppIDapp_ + 16位随机字符串
String appId = "app_" + CommonUtil.randomUUID16();
// 生成 AppSecret32位随机字符串大小写字母+数字)
String appSecret = generateSecretValue();
credential.setAppId(appId);
credential.setAppSecret(appSecret);
// 默认状态正常
if (credential.getStatus() == null) {
credential.setStatus(0);
}
if (StrUtil.isBlank(credential.getType())) {
credential.setType("server");
}
save(credential);
log.info("创建凭证成功websiteId={}, appId={}", credential.getWebsiteId(), appId);
// 返回含明文 secret 的对象(仅此一次)
return credential;
}
@Override
public AppCredential resetSecret(Long id) {
AppCredential credential = getById(id);
if (credential == null) {
throw new RuntimeException("凭证不存在");
}
String newSecret = generateSecretValue();
credential.setAppSecret(newSecret);
updateById(credential);
log.info("重置凭证密钥成功id={}", id);
return credential;
}
@Override
public boolean updateStatus(Long id, Integer status) {
return update(new LambdaUpdateWrapper<AppCredential>()
.eq(AppCredential::getId, id)
.set(AppCredential::getStatus, status));
}
/**
* 生成32位随机 AppSecret
*/
private String generateSecretValue() {
// 格式8位-8位-8位-8位类似 UUID 格式,便于复制)
return RandomUtil.randomString(8) + "-"
+ RandomUtil.randomString(8) + "-"
+ RandomUtil.randomString(8) + "-"
+ RandomUtil.randomString(8);
}
/**
* 脱敏处理:将 appSecret 替换为掩码
*/
private void maskSecret(AppCredential credential) {
if (StrUtil.isNotBlank(credential.getAppSecret())) {
// 只保留前4位其余用 * 替代
String secret = credential.getAppSecret();
credential.setAppSecret(secret.substring(0, Math.min(4, secret.length())) + "**********************");
}
}
}

View File

@@ -0,0 +1,86 @@
package com.gxwebsoft.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.app.mapper.AppEventMapper;
import com.gxwebsoft.app.service.AppEventService;
import com.gxwebsoft.app.entity.AppEvent;
import com.gxwebsoft.app.param.AppEventParam;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 应用操作动态Service实现
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Service
public class AppEventServiceImpl extends ServiceImpl<AppEventMapper, AppEvent> implements AppEventService {
@Override
public PageResult<AppEvent> pageRel(AppEventParam param) {
PageParam<AppEvent, AppEventParam> page = new PageParam<>(param);
page.setDefaultOrder("create_time desc");
List<AppEvent> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<AppEvent> listRel(AppEventParam param) {
List<AppEvent> list = baseMapper.selectListRel(param);
PageParam<AppEvent, AppEventParam> page = new PageParam<>();
page.setDefaultOrder("create_time desc");
return page.sortRecords(list);
}
@Override
public AppEvent getByIdRel(Integer id) {
AppEventParam param = new AppEventParam();
param.setId(id.longValue());
return param.getOne(baseMapper.selectListRel(param));
}
@Override
@Async
public void logEvent(Long websiteId, String eventType, String title, String content,
Long operatorId, String operatorName, Long refId, String refType) {
try {
AppEvent event = new AppEvent();
event.setWebsiteId(websiteId);
event.setEventType(eventType);
event.setTitle(title);
event.setContent(content);
event.setOperatorId(operatorId);
event.setOperator(operatorName);
event.setRefId(refId);
event.setRefType(refType);
event.setStatus(0);
save(event);
log.debug("记录事件成功websiteId={}, eventType={}, title={}", websiteId, eventType, title);
} catch (Exception e) {
log.warn("记录事件失败websiteId={}, eventType={}: {}", websiteId, eventType, e.getMessage());
}
}
@Override
@Async
public void logEvent(Long websiteId, String eventType, String title, Long operatorId, String operatorName) {
logEvent(websiteId, eventType, title, null, operatorId, operatorName, null, null);
}
@Override
public AppEvent getLatestEvent(Long websiteId) {
return getOne(new LambdaQueryWrapper<AppEvent>()
.eq(AppEvent::getWebsiteId, websiteId)
.orderByDesc(AppEvent::getCreateTime)
.last("LIMIT 1"));
}
}

View File

@@ -0,0 +1,109 @@
package com.gxwebsoft.app.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.app.entity.AppResource;
import com.gxwebsoft.app.mapper.AppResourceMapper;
import com.gxwebsoft.app.param.AppResourceParam;
import com.gxwebsoft.app.service.AppResourceService;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 开发者资源 Service 实现
*
* @author 科技小王子
* @since 2026-03-31
*/
@Slf4j
@Service
public class AppResourceServiceImpl extends ServiceImpl<AppResourceMapper, AppResource>
implements AppResourceService {
@Override
public PageResult<AppResource> pageRel(AppResourceParam param) {
PageParam<AppResource, AppResourceParam> page = new PageParam<>(param);
page.setDefaultOrder("create_time desc");
List<AppResource> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<AppResource> listRel(AppResourceParam param) {
return baseMapper.selectListRel(param);
}
@Override
public AppResource getByIdRel(Long resourceId) {
AppResourceParam param = new AppResourceParam();
param.setResourceId(resourceId);
List<AppResource> list = baseMapper.selectListRel(param);
return list.isEmpty() ? null : list.get(0);
}
@Override
@Transactional(rollbackFor = Exception.class)
public AppResource addResource(AppResource resource, Integer userId) {
resource.setUserId(userId);
resource.setDeleted(0);
resource.setCreateTime(LocalDateTime.now());
resource.setUpdateTime(LocalDateTime.now());
// 默认状态
if (resource.getStatus() == null) {
resource.setStatus("running");
}
save(resource);
log.info("新增资源成功, type={}, name={}, userId={}", resource.getResourceType(), resource.getName(), userId);
return resource;
}
@Override
@Transactional(rollbackFor = Exception.class)
public AppResource updateResource(AppResource resource) {
if (resource.getResourceId() == null) {
throw new RuntimeException("资源ID不能为空");
}
resource.setUpdateTime(LocalDateTime.now());
updateById(resource);
return getByIdRel(resource.getResourceId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeResource(Long resourceId, Integer userId) {
AppResource resource = getById(resourceId);
if (resource == null) {
throw new RuntimeException("资源不存在");
}
if (!resource.getUserId().equals(userId)) {
throw new RuntimeException("无权操作此资源");
}
resource.setDeleted(1);
resource.setUpdateTime(LocalDateTime.now());
updateById(resource);
log.info("删除资源成功, resourceId={}, userId={}", resourceId, userId);
}
@Override
public Map<String, Long> countByType(Integer userId, Integer tenantId) {
List<Map<String, Object>> raw = baseMapper.countByType(userId, tenantId);
Map<String, Long> result = new HashMap<>();
// 初始化所有类型为 0
for (String type : new String[]{"server", "database", "storage", "domain", "ssl"}) {
result.put(type, 0L);
}
for (Map<String, Object> row : raw) {
String type = (String) row.get("resourceType");
Long cnt = ((Number) row.get("cnt")).longValue();
result.put(type, cnt);
}
return result;
}
}

View File

@@ -0,0 +1,579 @@
package com.gxwebsoft.app.service.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.app.entity.AppTicket;
import com.gxwebsoft.app.entity.AppTicketReply;
import com.gxwebsoft.app.entity.AppUser;
import com.gxwebsoft.app.mapper.AppTicketMapper;
import com.gxwebsoft.app.mapper.AppTicketReplyMapper;
import com.gxwebsoft.app.param.AppTicketParam;
import com.gxwebsoft.app.service.AppTicketService;
import com.gxwebsoft.app.service.AppUserService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
* 应用工单 Service 实现
*
* @author 科技小王子
* @since 2026-03-30
*/
@Slf4j
@Service
public class AppTicketServiceImpl extends ServiceImpl<AppTicketMapper, AppTicket> implements AppTicketService {
/** 企业微信群机器人 Webhook留空则不发送 */
@Value("${notify.wecom-webhook:}")
private String wecomWebhook;
/** 飞书群机器人 Webhook留空则不发送 */
@Value("${notify.feishu-webhook:}")
private String feishuWebhook;
@Resource
private AppTicketReplyMapper replyMapper;
@Resource
private AppUserService appUserService;
@Resource
private UserService userService;
// ─── 生成工单编号 ─────────────────────────────────────────────
private String generateTicketNo() {
String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String rand = String.format("%04d", new Random().nextInt(10000));
return "TK-" + ts + rand;
}
// ─── 客户端:查自己的工单 ─────────────────────────────────────
@Override
public PageResult<AppTicket> myPage(AppTicketParam param, Integer userId) {
// 用 PageParam 包装分页参数getCurrent/getSize 来自父类 Page<T>
Page<AppTicket> page = new Page<>(param.getCurrent(), param.getSize() > 0 ? param.getSize() : 15);
LambdaQueryWrapper<AppTicket> wrapper = new LambdaQueryWrapper<AppTicket>()
.eq(AppTicket::getDeleted, 0)
.eq(AppTicket::getSubmitUserId, userId)
.eq(ObjectUtil.isNotNull(param.getWebsiteId()), AppTicket::getWebsiteId, param.getWebsiteId())
.eq(ObjectUtil.isNotEmpty(param.getStatus()), AppTicket::getStatus, param.getStatus())
.eq(ObjectUtil.isNotEmpty(param.getCategory()), AppTicket::getCategory, param.getCategory())
.and(ObjectUtil.isNotEmpty(param.getKeywords()), q ->
q.like(AppTicket::getTitle, param.getKeywords())
.or().like(AppTicket::getTicketNo, param.getKeywords()))
// 状态优先级pending > assigned > processing > resolved > closed同状态内最新的排前
.last("ORDER BY FIELD(status,'pending','assigned','processing','resolved','closed','rejected'), create_time DESC");
baseMapper.selectPage(page, wrapper);
return new PageResult<>(page.getRecords(), page.getTotal());
}
// ─── 技术端:查所有工单 ───────────────────────────────────────
@Override
public PageResult<AppTicket> allPage(AppTicketParam param) {
Page<AppTicket> page = new Page<>(param.getCurrent(), param.getSize() > 0 ? param.getSize() : 20);
LambdaQueryWrapper<AppTicket> wrapper = new LambdaQueryWrapper<AppTicket>()
.eq(AppTicket::getDeleted, 0)
.eq(ObjectUtil.isNotNull(param.getWebsiteId()), AppTicket::getWebsiteId, param.getWebsiteId())
.eq(ObjectUtil.isNotEmpty(param.getStatus()), AppTicket::getStatus, param.getStatus())
.eq(ObjectUtil.isNotEmpty(param.getCategory()), AppTicket::getCategory, param.getCategory())
.eq(ObjectUtil.isNotEmpty(param.getPriority()), AppTicket::getPriority, param.getPriority())
.and(ObjectUtil.isNotNull(param.getAssigneeId()) && param.getAssigneeId() == 0,
q -> q.isNull(AppTicket::getAssigneeId))
.eq(ObjectUtil.isNotNull(param.getAssigneeId()) && param.getAssigneeId() > 0,
AppTicket::getAssigneeId, param.getAssigneeId())
.and(ObjectUtil.isNotEmpty(param.getKeywords()), q ->
q.like(AppTicket::getTitle, param.getKeywords())
.or().like(AppTicket::getTicketNo, param.getKeywords()))
// 状态优先级 > 紧急程度 > 最新提交时间
.last("ORDER BY FIELD(status,'pending','assigned','processing','resolved','closed','rejected'), FIELD(priority,'urgent','high','normal','low'), create_time DESC");
baseMapper.selectPage(page, wrapper);
return new PageResult<>(page.getRecords(), page.getTotal());
}
// ─── 提交工单(自动分配) ─────────────────────────────────────
@Override
@Transactional(rollbackFor = Exception.class)
public AppTicket submit(AppTicket ticket, Integer userId) {
// 补充提交人信息
ticket.setSubmitUserId(userId);
ticket.setTicketNo(generateTicketNo());
ticket.setStatus("pending");
ticket.setReplyCount(0);
ticket.setDeleted(0);
ticket.setCreateTime(LocalDateTime.now());
ticket.setUpdateTime(LocalDateTime.now());
// 从用户服务取昵称/头像冗余存储(@InterceptorIgnore 绕过租户拦截器跨库查询)
try {
User user = userService.getByIdIgnoreTenant(userId);
if (user != null) {
ticket.setSubmitUserName(user.getNickname() != null ? user.getNickname() : user.getUsername());
ticket.setSubmitUserAvatar(user.getAvatar());
}
} catch (Exception e) {
log.warn("获取提交人信息失败", e);
}
// 自动分配:查找该应用的 admin/developer 成员,随机分配
autoAssign(ticket);
save(ticket);
// 异步推送:新工单通知(通知分配到的技术人员)
sendTicketCreatedAsync(ticket);
return ticket;
}
/** 自动分配工单给应用的技术成员 */
private void autoAssign(AppTicket ticket) {
if (ticket.getWebsiteId() == null) return;
try {
List<AppUser> members = appUserService.list(
new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getWebsiteId, ticket.getWebsiteId())
.eq(AppUser::getStatus, 0)
.in(AppUser::getRole, "admin", "developer", "owner"));
if (!members.isEmpty()) {
// 简单轮询:按工单数最少分配(此处随机)
AppUser assigned = members.get(new Random().nextInt(members.size()));
ticket.setAssigneeId(assigned.getUserId());
ticket.setAssigneeName(assigned.getNickname() != null ? assigned.getNickname() : assigned.getUsername());
ticket.setAssigneeAvatar(assigned.getAvatar());
ticket.setStatus("assigned");
}
} catch (Exception e) {
log.warn("自动分配工单失败,保持 pending 状态", e);
}
}
// ─── 更新状态 ─────────────────────────────────────────────────
@Override
public void updateStatus(Long ticketId, String status, Integer operatorId) {
LambdaUpdateWrapper<AppTicket> wrapper = new LambdaUpdateWrapper<AppTicket>()
.eq(AppTicket::getTicketId, ticketId)
.set(AppTicket::getStatus, status)
.set(AppTicket::getUpdateTime, LocalDateTime.now());
if ("resolved".equals(status)) {
wrapper.set(AppTicket::getResolvedTime, LocalDateTime.now());
// 如果没有分配人,自动将操作人设为处理人
AppTicket t = getById(ticketId);
if (t != null && t.getAssigneeId() == null) {
wrapper.set(AppTicket::getAssigneeId, operatorId);
}
}
update(wrapper);
// 异步推送:状态变更通知提交人(已解决/已关闭)
if ("resolved".equals(status) || "closed".equals(status)) {
AppTicket t = getById(ticketId);
if (t != null) sendStatusChangedAsync(t, status);
}
}
// ─── 分配处理人 ───────────────────────────────────────────────
@Override
public void assign(Long ticketId, Integer assigneeId) {
User user = userService.getByIdIgnoreTenant(assigneeId);
LambdaUpdateWrapper<AppTicket> wrapper = new LambdaUpdateWrapper<AppTicket>()
.eq(AppTicket::getTicketId, ticketId)
.set(AppTicket::getAssigneeId, assigneeId)
.set(user != null, AppTicket::getAssigneeName,
user != null ? (user.getNickname() != null ? user.getNickname() : user.getUsername()) : null)
.set(user != null, AppTicket::getAssigneeAvatar, user != null ? user.getAvatar() : null)
.set(AppTicket::getStatus, "assigned")
.set(AppTicket::getUpdateTime, LocalDateTime.now());
update(wrapper);
// 异步推送:通知新分配到的技术人员
AppTicket t = getById(ticketId);
if (t != null) sendTicketAssignedAsync(t);
}
// ─── 用户关闭工单 ─────────────────────────────────────────────
@Override
public void closeByUser(Long ticketId, Integer userId) {
AppTicket ticket = getById(ticketId);
if (ticket == null || !ticket.getSubmitUserId().equals(userId)) {
throw new RuntimeException("无权操作该工单");
}
update(new LambdaUpdateWrapper<AppTicket>()
.eq(AppTicket::getTicketId, ticketId)
.set(AppTicket::getStatus, "closed")
.set(AppTicket::getClosedTime, LocalDateTime.now())
.set(AppTicket::getUpdateTime, LocalDateTime.now()));
}
// ─── 获取回复列表 ─────────────────────────────────────────────
@Override
public List<AppTicketReply> getReplies(Long ticketId) {
return replyMapper.selectList(
new LambdaQueryWrapper<AppTicketReply>()
.eq(AppTicketReply::getTicketId, ticketId)
.eq(AppTicketReply::getDeleted, 0)
.orderByAsc(AppTicketReply::getCreateTime));
}
// ─── 添加回复 ─────────────────────────────────────────────────
@Override
@Transactional(rollbackFor = Exception.class)
public AppTicketReply addReply(AppTicketReply reply, Integer userId) {
reply.setUserId(userId);
reply.setDeleted(0);
reply.setCreateTime(LocalDateTime.now());
// 补充用户信息(@InterceptorIgnore 绕过租户拦截器跨库查询)
try {
User user = userService.getByIdIgnoreTenant(userId);
if (user != null) {
reply.setUserName(user.getNickname() != null ? user.getNickname() : user.getUsername());
reply.setUserAvatar(user.getAvatar());
}
} catch (Exception e) {
log.warn("获取回复人信息失败", e);
}
// 判断是否是技术人员(该应用的 admin/developer/owner
AppTicket ticket = getById(reply.getTicketId());
if (ticket != null) {
boolean isStaff = appUserService.count(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getWebsiteId, ticket.getWebsiteId())
.eq(AppUser::getUserId, userId)
.in(AppUser::getRole, "owner", "admin", "developer")
.eq(AppUser::getStatus, 0)) > 0;
reply.setIsStaff(isStaff ? 1 : 0);
} else {
reply.setIsStaff(0);
}
replyMapper.insert(reply);
// 更新工单回复数 & 更新时间,若状态是 assigned 则推进为 processing
update(new LambdaUpdateWrapper<AppTicket>()
.eq(AppTicket::getTicketId, reply.getTicketId())
.setSql("reply_count = reply_count + 1")
.set(AppTicket::getUpdateTime, LocalDateTime.now())
.eq(AppTicket::getStatus, "assigned")
.set(AppTicket::getStatus, "processing"));
// 异步推送:有新回复时通知对方
if (ticket != null) sendReplyNotifyAsync(ticket, reply);
return reply;
}
// ─── 统计 ─────────────────────────────────────────────────────
@Override
public Map<String, Long> stats(Long websiteId, Integer userId) {
LambdaQueryWrapper<AppTicket> base = new LambdaQueryWrapper<AppTicket>()
.eq(AppTicket::getDeleted, 0)
.eq(ObjectUtil.isNotNull(websiteId), AppTicket::getWebsiteId, websiteId)
.eq(ObjectUtil.isNotNull(userId), AppTicket::getSubmitUserId, userId);
Map<String, Long> result = new HashMap<>();
result.put("total", (long) count(base.clone()));
result.put("pending", (long) count(base.clone().in(AppTicket::getStatus, "pending", "assigned")));
result.put("processing", (long) count(base.clone().eq(AppTicket::getStatus, "processing")));
result.put("resolved", (long) count(base.clone().eq(AppTicket::getStatus, "resolved")));
result.put("closed", (long) count(base.clone().eq(AppTicket::getStatus, "closed")));
return result;
}
// ─── 获取技术人员列表 ─────────────────────────────────────────
@Override
public List<Map<String, Object>> getTechStaffList() {
List<AppUser> members = appUserService.list(
new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getStatus, 0)
.in(AppUser::getRole, "owner", "admin", "developer")
.select(AppUser::getUserId, AppUser::getNickname, AppUser::getAvatar));
// 去重(一个用户可能在多个应用里)
Map<Integer, AppUser> dedupMap = new LinkedHashMap<>();
for (AppUser m : members) {
dedupMap.put(m.getUserId(), m);
}
return dedupMap.values().stream().map(m -> {
Map<String, Object> map = new HashMap<>();
map.put("userId", m.getUserId());
map.put("nickname", m.getNickname() != null ? m.getNickname() : "用户" + m.getUserId());
map.put("avatar", m.getAvatar());
return map;
}).collect(Collectors.toList());
}
// ════════════════════════════════════════════════════════════
// 异步消息推送
// ════════════════════════════════════════════════════════════
/**
* 场景1新工单提交 → 通知分配到的技术人员
*/
@Async
public void sendTicketCreatedAsync(AppTicket ticket) {
String now = now();
String title = String.format("🎫 新工单待处理(%s", now);
String assigneeLine = StrUtil.isNotBlank(ticket.getAssigneeName())
? "**处理人:** " + ticket.getAssigneeName()
: "**处理人:** 待分配";
String wecomContent = String.format(
"## %s\n" +
"> **工单号:** %s\n" +
"> **标题:** %s\n" +
"> **分类:** %s **优先级:** %s\n" +
"> **提交人:** %s\n" +
"> %s\n" +
"> **描述:** %s",
title,
nullSafe(ticket.getTicketNo()),
nullSafe(ticket.getTitle()),
categoryLabel(ticket.getCategory()), priorityLabel(ticket.getPriority()),
nullSafe(ticket.getSubmitUserName()),
assigneeLine,
truncate(ticket.getContent(), 120)
);
List<String[]> feishuLines = Arrays.asList(
new String[]{"工单号:" + nullSafe(ticket.getTicketNo())},
new String[]{"标 题:" + nullSafe(ticket.getTitle())},
new String[]{"分 类:" + categoryLabel(ticket.getCategory()) + " 优先级:" + priorityLabel(ticket.getPriority())},
new String[]{"提交人:" + nullSafe(ticket.getSubmitUserName())},
new String[]{assigneeLine.replace("**", "")},
new String[]{"描 述:" + truncate(ticket.getContent(), 120)}
);
doSend(title, wecomContent, feishuLines, "ticket_created");
}
/**
* 场景2工单被重新分配 → 通知新处理人
*/
@Async
public void sendTicketAssignedAsync(AppTicket ticket) {
String now = now();
String title = String.format("📋 工单已分配给您(%s", now);
String wecomContent = String.format(
"## %s\n" +
"> **工单号:** %s\n" +
"> **标题:** %s\n" +
"> **分配给:** <font color=\"warning\">%s</font>\n" +
"> **分类:** %s **优先级:** %s\n" +
"> **描述:** %s",
title,
nullSafe(ticket.getTicketNo()),
nullSafe(ticket.getTitle()),
nullSafe(ticket.getAssigneeName()),
categoryLabel(ticket.getCategory()), priorityLabel(ticket.getPriority()),
truncate(ticket.getContent(), 100)
);
List<String[]> feishuLines = Arrays.asList(
new String[]{"工单号:" + nullSafe(ticket.getTicketNo())},
new String[]{"标 题:" + nullSafe(ticket.getTitle())},
new String[]{"分配给:" + nullSafe(ticket.getAssigneeName())},
new String[]{"分 类:" + categoryLabel(ticket.getCategory()) + " 优先级:" + priorityLabel(ticket.getPriority())},
new String[]{"描 述:" + truncate(ticket.getContent(), 100)}
);
doSend(title, wecomContent, feishuLines, "ticket_assigned");
}
/**
* 场景3有新回复 → 技术人员回复则通知客户,客户回复则通知技术人员
*/
@Async
public void sendReplyNotifyAsync(AppTicket ticket, AppTicketReply reply) {
boolean isStaff = Integer.valueOf(1).equals(reply.getIsStaff());
String now = now();
String who = isStaff ? "技术人员" : "用户";
String notifyRole = isStaff ? "您的工单有新回复" : "工单有用户新回复";
String title = String.format("💬 %s%s", notifyRole, now);
String wecomContent = String.format(
"## %s\n" +
"> **工单号:** %s\n" +
"> **标题:** %s\n" +
"> **回复人:** %s%s\n" +
"> **回复内容:** %s",
title,
nullSafe(ticket.getTicketNo()),
nullSafe(ticket.getTitle()),
nullSafe(reply.getUserName()), who,
truncate(reply.getContent(), 200)
);
List<String[]> feishuLines = Arrays.asList(
new String[]{"工单号:" + nullSafe(ticket.getTicketNo())},
new String[]{"标 题:" + nullSafe(ticket.getTitle())},
new String[]{"回复人:" + nullSafe(reply.getUserName()) + "" + who + ""},
new String[]{"回复内容:" + truncate(reply.getContent(), 200)}
);
doSend(title, wecomContent, feishuLines, "ticket_reply");
}
/**
* 场景4状态变更已解决/已关闭)→ 通知提交人
*/
@Async
public void sendStatusChangedAsync(AppTicket ticket, String status) {
String now = now();
String emoji = "resolved".equals(status) ? "" : "🔒";
String statusLabel = "resolved".equals(status) ? "已解决" : "已关闭";
String title = String.format("%s 工单%s%s", emoji, statusLabel, now);
String wecomContent = String.format(
"## %s\n" +
"> **工单号:** %s\n" +
"> **标题:** %s\n" +
"> **提交人:** %s\n" +
"> **处理人:** %s\n" +
"> **状态:** <font color=\"%s\">%s</font>",
title,
nullSafe(ticket.getTicketNo()),
nullSafe(ticket.getTitle()),
nullSafe(ticket.getSubmitUserName()),
nullSafe(ticket.getAssigneeName()),
"resolved".equals(status) ? "info" : "comment",
statusLabel
);
List<String[]> feishuLines = Arrays.asList(
new String[]{"工单号:" + nullSafe(ticket.getTicketNo())},
new String[]{"标 题:" + nullSafe(ticket.getTitle())},
new String[]{"提交人:" + nullSafe(ticket.getSubmitUserName())},
new String[]{"处理人:" + nullSafe(ticket.getAssigneeName())},
new String[]{"状 态:" + statusLabel}
);
doSend(title, wecomContent, feishuLines, "ticket_status");
}
/**
* 统一发送入口:企业微信 + 飞书
*/
private void doSend(String title, String wecomContent, List<String[]> feishuLines, String scene) {
if (StrUtil.isNotBlank(wecomWebhook)) {
try {
sendWecom(wecomContent);
} catch (Exception e) {
log.warn("[工单通知][{}] 企业微信发送失败: {}", scene, e.getMessage());
}
}
if (StrUtil.isNotBlank(feishuWebhook)) {
try {
sendFeishu(title, feishuLines);
} catch (Exception e) {
log.warn("[工单通知][{}] 飞书发送失败: {}", scene, e.getMessage());
}
}
}
/**
* 企业微信群机器人 - markdown 消息
* 文档https://developer.work.weixin.qq.com/document/path/91770
*/
private void sendWecom(String content) {
Map<String, Object> textMap = new HashMap<>();
textMap.put("content", content);
Map<String, Object> payload = new HashMap<>();
payload.put("msgtype", "markdown");
payload.put("markdown", textMap);
String resp = HttpUtil.post(wecomWebhook, JSON.toJSONString(payload));
log.info("[工单通知] 企业微信推送结果: {}", resp);
}
/**
* 飞书群机器人 - 富文本post消息
* 文档https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
*/
private void sendFeishu(String title, List<String[]> lines) {
List<List<Map<String, String>>> content = new ArrayList<>();
for (String[] line : lines) {
Map<String, String> node = new HashMap<>();
node.put("tag", "text");
node.put("text", line[0]);
content.add(Collections.singletonList(node));
}
Map<String, Object> zhCn = new HashMap<>();
zhCn.put("title", title);
zhCn.put("content", content);
Map<String, Object> postContent = new HashMap<>();
postContent.put("zh_cn", zhCn);
Map<String, Object> post = new HashMap<>();
post.put("content", postContent);
Map<String, Object> payload = new HashMap<>();
payload.put("msg_type", "post");
payload.put("content", post);
String resp = HttpUtil.post(feishuWebhook, JSON.toJSONString(payload));
log.info("[工单通知] 飞书推送结果: {}", resp);
}
// ════════════════════════════════════════════════════════════
// 工具方法
// ════════════════════════════════════════════════════════════
private String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("MM-dd HH:mm"));
}
private String nullSafe(String s) {
return StrUtil.isBlank(s) ? "" : s;
}
private String truncate(String s, int max) {
if (StrUtil.isBlank(s)) return "";
return s.length() > max ? s.substring(0, max) + "" : s;
}
private String categoryLabel(String category) {
if (category == null) return "其他";
return switch (category) {
case "bug" -> "Bug反馈";
case "feature" -> "功能需求";
case "config" -> "配置问题";
case "performance" -> "性能问题";
case "security" -> "安全问题";
default -> category;
};
}
private String priorityLabel(String priority) {
if (priority == null) return "普通";
return switch (priority) {
case "urgent" -> "🔴 紧急";
case "high" -> "🟠 高";
case "normal" -> "🟡 普通";
case "low" -> "🟢 低";
default -> priority;
};
}
}

View File

@@ -0,0 +1,112 @@
package com.gxwebsoft.app.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.app.mapper.AppUserMapper;
import com.gxwebsoft.app.service.AppUserService;
import com.gxwebsoft.app.entity.AppUser;
import com.gxwebsoft.app.param.AppUserParam;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
/**
* 应用成员Service实现
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Service
public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> implements AppUserService {
/** 注入同 jar 包内的 UserService用于写入冗余用户信息不做跨库 JOIN */
@Resource
private UserService userService;
@Override
public PageResult<AppUser> pageRel(AppUserParam param) {
PageParam<AppUser, AppUserParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time asc");
List<AppUser> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<AppUser> listRel(AppUserParam param) {
List<AppUser> list = baseMapper.selectListRel(param);
PageParam<AppUser, AppUserParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time asc");
return page.sortRecords(list);
}
@Override
public AppUser getByIdRel(Integer id) {
AppUserParam param = new AppUserParam();
param.setId(id.longValue());
return param.getOne(baseMapper.selectListRel(param));
}
@Override
public AppUser inviteUser(Long websiteId, Integer userId, String role, Integer inviteBy) {
// 检查是否已经是成员
if (isMember(websiteId, userId)) {
throw new RuntimeException("该用户已经是应用成员");
}
// 查询用户基础信息并冗余写入,避免跨库 JOIN
User sysUser = userService.getByIdIgnoreTenant(userId);
if (sysUser == null) {
throw new RuntimeException("用户不存在userId=" + userId);
}
AppUser appUser = new AppUser();
appUser.setWebsiteId(websiteId);
appUser.setUserId(userId);
appUser.setRole(role != null ? role : "developer");
appUser.setInviteBy(inviteBy.longValue());
appUser.setInviteTime(LocalDateTime.now());
appUser.setStatus(0);
appUser.setSortNumber(0);
// 冗余写入用户基础信息,彻底解除跨库 JOIN 依赖
appUser.setUsername(sysUser.getUsername());
appUser.setNickname(sysUser.getNickname());
appUser.setAvatar(sysUser.getAvatar());
appUser.setPhone(sysUser.getPhone());
save(appUser);
log.info("邀请成员成功websiteId={}, userId={}, username={}, role={}", websiteId, userId, sysUser.getUsername(), role);
return appUser;
}
@Override
public boolean updateRole(Long id, String role) {
return update(new LambdaUpdateWrapper<AppUser>()
.eq(AppUser::getId, id)
.set(AppUser::getRole, role));
}
@Override
public boolean isMember(Long websiteId, Integer userId) {
long count = count(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getWebsiteId, websiteId)
.eq(AppUser::getUserId, userId)
.eq(AppUser::getStatus, 0));
return count > 0;
}
@Override
public User findUserByPhone(String phone) {
return userService.getByPhone(phone);
}
}

View File

@@ -0,0 +1,112 @@
package com.gxwebsoft.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.app.mapper.AppVersionMapper;
import com.gxwebsoft.app.service.AppVersionService;
import com.gxwebsoft.app.entity.AppVersion;
import com.gxwebsoft.app.param.AppVersionParam;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
* 应用版本发布记录Service实现
*
* @author 科技小王子
* @since 2026-03-28 21:29:44
*/
@Slf4j
@Service
public class AppVersionServiceImpl extends ServiceImpl<AppVersionMapper, AppVersion> implements AppVersionService {
@Override
public PageResult<AppVersion> pageRel(AppVersionParam param) {
PageParam<AppVersion, AppVersionParam> page = new PageParam<>(param);
page.setDefaultOrder("create_time desc");
List<AppVersion> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<AppVersion> listRel(AppVersionParam param) {
List<AppVersion> list = baseMapper.selectListRel(param);
PageParam<AppVersion, AppVersionParam> page = new PageParam<>();
page.setDefaultOrder("create_time desc");
return page.sortRecords(list);
}
@Override
public AppVersion getByIdRel(Integer id) {
AppVersionParam param = new AppVersionParam();
param.setId(id.longValue());
return param.getOne(baseMapper.selectListRel(param));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean publish(Long id, Integer publishBy) {
AppVersion version = getById(id);
if (version == null) {
throw new RuntimeException("版本不存在");
}
Long websiteId = version.getWebsiteId();
// 1. 将该应用下所有版本的 isCurrent 设为 false
update(new LambdaUpdateWrapper<AppVersion>()
.eq(AppVersion::getWebsiteId, websiteId)
.set(AppVersion::getIsCurrent, false));
// 2. 将当前版本设为已发布+当前版本
version.setStatus(1);
version.setIsCurrent(true);
version.setPublishBy(publishBy.longValue());
version.setPublishTime(LocalDateTime.now());
boolean result = updateById(version);
log.info("发布版本成功id={}, versionNo={}", id, version.getVersionNo());
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean rollback(Long id, Integer publishBy) {
AppVersion targetVersion = getById(id);
if (targetVersion == null) {
throw new RuntimeException("目标版本不存在");
}
Long websiteId = targetVersion.getWebsiteId();
// 1. 将当前版本标记为已回滚
update(new LambdaUpdateWrapper<AppVersion>()
.eq(AppVersion::getWebsiteId, websiteId)
.eq(AppVersion::getIsCurrent, true)
.set(AppVersion::getStatus, 2) // 2=已回滚
.set(AppVersion::getIsCurrent, false));
// 2. 将目标版本设为当前版本
targetVersion.setStatus(1);
targetVersion.setIsCurrent(true);
targetVersion.setPublishBy(publishBy.longValue());
targetVersion.setPublishTime(LocalDateTime.now());
boolean result = updateById(targetVersion);
log.info("回滚版本成功id={}, versionNo={}", id, targetVersion.getVersionNo());
return result;
}
@Override
public AppVersion getCurrentVersion(Long websiteId) {
return getOne(new LambdaQueryWrapper<AppVersion>()
.eq(AppVersion::getWebsiteId, websiteId)
.eq(AppVersion::getIsCurrent, true)
.last("LIMIT 1"));
}
}

View File

@@ -0,0 +1,54 @@
-- ----------------------------
-- 开发者资源管理表
-- 统一存放服务器/数据库/云存储/域名/SSL证书等开发基础设施资源
-- ----------------------------
CREATE TABLE IF NOT EXISTS `app_resource` (
`resource_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '资源ID',
`resource_type` VARCHAR(20) NOT NULL COMMENT '资源类型: server/database/storage/domain/ssl',
`name` VARCHAR(100) NOT NULL COMMENT '资源名称',
`provider` VARCHAR(30) DEFAULT NULL COMMENT '服务商: tencent/aliyun/huawei/other',
-- 服务器字段
`ip` VARCHAR(50) DEFAULT NULL COMMENT 'IP地址服务器用',
-- 数据库字段
`db_type` VARCHAR(30) DEFAULT NULL COMMENT '数据库类型: MySQL/PostgreSQL/Redis/MongoDB',
`host` VARCHAR(200) DEFAULT NULL COMMENT '连接主机地址(数据库用)',
`port` INT DEFAULT NULL COMMENT '连接端口(数据库用)',
-- 云存储字段
`region` VARCHAR(50) DEFAULT NULL COMMENT '地区/Region云存储用',
`acl` VARCHAR(30) DEFAULT 'private' COMMENT '访问权限: public-read/private云存储用',
`used_bytes` BIGINT DEFAULT 0 COMMENT '已用空间(字节,云存储用)',
-- 域名字段
`domain` VARCHAR(200) DEFAULT NULL COMMENT '域名(域名/SSL用',
`registrar` VARCHAR(100) DEFAULT NULL COMMENT '注册商(域名用)',
`icp` TINYINT(1) DEFAULT 0 COMMENT '是否已备案: 0否 1是域名用',
`icp_no` VARCHAR(100) DEFAULT NULL COMMENT 'ICP备案号域名用',
`ssl_bound` TINYINT(1) DEFAULT 0 COMMENT '是否已绑定SSL域名冗余标记',
-- SSL证书字段
`cert_type` VARCHAR(10) DEFAULT NULL COMMENT '证书类型: DV/OV/EVSSL用',
`issuer` VARCHAR(100) DEFAULT NULL COMMENT '颁发机构SSL用',
-- 通用字段
`status` VARCHAR(20) NOT NULL DEFAULT 'running' COMMENT '状态: running/stopped/expired/pending',
`website_id` BIGINT DEFAULT NULL COMMENT '关联应用ID可选',
`expire_at` DATE DEFAULT NULL COMMENT '到期时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
`user_id` INT NOT NULL COMMENT '所属用户ID',
`tenant_id` INT DEFAULT NULL COMMENT '租户ID',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`resource_id`),
KEY `idx_resource_type` (`resource_type`),
KEY `idx_user_id` (`user_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_website_id` (`website_id`),
KEY `idx_status` (`status`),
KEY `idx_expire_at` (`expire_at`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开发者资源管理(服务器/数据库/云存储/域名/SSL';

View File

@@ -1,104 +0,0 @@
package com.gxwebsoft.auto.controller;
import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
import com.gxwebsoft.auto.service.QrLoginService;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.ApiResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* 认证模块
*
* @author 科技小王子
* @since 2025-03-06 22:50:25
*/
@Tag(name = "认证模块")
@RestController
@RequestMapping("/api/qr-login")
public class QrLoginController extends BaseController {
@Autowired
private QrLoginService qrLoginService;
/**
* 生成扫码登录token
*/
@Operation(summary = "生成扫码登录token")
@PostMapping("/generate")
public ApiResult<?> generateQrLoginToken() {
try {
QrLoginGenerateResponse response = qrLoginService.generateQrLoginToken();
return success("生成成功", response);
} catch (Exception e) {
return fail(e.getMessage());
}
}
/**
* 检查扫码登录状态
*/
@Operation(summary = "检查扫码登录状态")
@GetMapping("/status/{token}")
public ApiResult<?> checkQrLoginStatus(
@Parameter(description = "扫码登录token") @PathVariable String token) {
try {
QrLoginStatusResponse response = qrLoginService.checkQrLoginStatus(token);
return success("查询成功", response);
} catch (Exception e) {
return fail(e.getMessage());
}
}
/**
* 确认扫码登录
*/
@Operation(summary = "确认扫码登录")
@PostMapping("/confirm")
public ApiResult<?> confirmQrLogin(@Valid @RequestBody QrLoginConfirmRequest request) {
try {
QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
return success("确认成功", response);
} catch (Exception e) {
return fail(e.getMessage());
}
}
/**
* 扫码操作(可选接口,用于移动端扫码后更新状态)
*/
@Operation(summary = "扫码操作")
@PostMapping("/scan/{token}")
public ApiResult<?> scanQrCode(@Parameter(description = "扫码登录token") @PathVariable String token) {
try {
boolean result = qrLoginService.scanQrCode(token);
return success("操作成功", result);
} catch (Exception e) {
return fail(e.getMessage());
}
}
/**
* 微信小程序扫码登录确认(便捷接口)
*/
@Operation(summary = "微信小程序扫码登录确认")
@PostMapping("/wechat-confirm")
public ApiResult<?> wechatMiniProgramConfirm(@Valid @RequestBody QrLoginConfirmRequest request) {
try {
// 设置平台为微信小程序
request.setPlatform("miniprogram");
QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
return success("微信小程序登录确认成功", response);
} catch (Exception e) {
return fail(e.getMessage());
}
}
}

View File

@@ -1,50 +0,0 @@
package com.gxwebsoft.auto.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 扫码登录确认请求
*
* @author 科技小王子
* @since 2025-08-31
*/
@Data
@Schema(description = "扫码登录确认请求")
public class QrLoginConfirmRequest {
@Schema(description = "扫码登录token")
@NotBlank(message = "token不能为空")
private String token;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "登录平台: web-网页端, app-移动应用, miniprogram-微信小程序")
private String platform;
@Schema(description = "微信小程序相关信息")
private WechatMiniProgramInfo wechatInfo;
/**
* 微信小程序信息
*/
@Data
@Schema(description = "微信小程序信息")
public static class WechatMiniProgramInfo {
@Schema(description = "微信openid")
private String openid;
@Schema(description = "微信unionid")
private String unionid;
@Schema(description = "微信昵称")
private String nickname;
@Schema(description = "微信头像")
private String avatar;
}
}

View File

@@ -1,55 +0,0 @@
package com.gxwebsoft.auto.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 扫码登录数据模型
*
* @author 科技小王子
* @since 2025-08-31
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class QrLoginData {
/**
* 扫码登录token
*/
private String token;
/**
* 状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期
*/
private String status;
/**
* 用户ID(扫码确认后设置)
*/
private Integer userId;
/**
* 用户名(扫码确认后设置)
*/
private String username;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* JWT访问令牌(确认后生成)
*/
private String accessToken;
}

View File

@@ -1,29 +0,0 @@
package com.gxwebsoft.auto.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 扫码登录生成响应
*
* @author 科技小王子
* @since 2025-08-31
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "扫码登录生成响应")
public class QrLoginGenerateResponse {
@Schema(description = "扫码登录token")
private String token;
@Schema(description = "二维码内容")
private String qrCode;
@Schema(description = "过期时间(秒)")
private Long expiresIn;
}

View File

@@ -1,32 +0,0 @@
package com.gxwebsoft.auto.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 扫码登录状态响应
*
* @author 科技小王子
* @since 2025-08-31
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "扫码登录状态响应")
public class QrLoginStatusResponse {
@Schema(description = "状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期")
private String status;
@Schema(description = "JWT访问令牌(仅在confirmed状态时返回)")
private String accessToken;
@Schema(description = "用户信息(仅在confirmed状态时返回)")
private Object userInfo;
@Schema(description = "剩余过期时间(秒)")
private Long expiresIn;
}

View File

@@ -1,46 +0,0 @@
package com.gxwebsoft.auto.service;
import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
/**
* 扫码登录服务接口
*
* @author 科技小王子
* @since 2025-08-31
*/
public interface QrLoginService {
/**
* 生成扫码登录token
*
* @return QrLoginGenerateResponse
*/
QrLoginGenerateResponse generateQrLoginToken();
/**
* 检查扫码登录状态
*
* @param token 扫码登录token
* @return QrLoginStatusResponse
*/
QrLoginStatusResponse checkQrLoginStatus(String token);
/**
* 确认扫码登录
*
* @param request 确认请求
* @return QrLoginStatusResponse
*/
QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request);
/**
* 扫码操作(更新状态为已扫码)
*
* @param token 扫码登录token
* @return boolean
*/
boolean scanQrCode(String token);
}

View File

@@ -1,239 +0,0 @@
package com.gxwebsoft.auto.service.impl;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import com.gxwebsoft.auto.dto.*;
import com.gxwebsoft.auto.service.QrLoginService;
import com.gxwebsoft.common.core.security.JwtSubject;
import com.gxwebsoft.common.core.security.JwtUtil;
import com.gxwebsoft.common.core.utils.JSONUtil;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import static com.gxwebsoft.common.core.constants.RedisConstants.*;
/**
* 扫码登录服务实现
*
* @author 科技小王子
* @since 2025-08-31
*/
@Slf4j
@Service
public class QrLoginServiceImpl implements QrLoginService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private UserService userService;
@Value("${config.jwt.secret:websoft-jwt-secret-key-2025}")
private String jwtSecret;
@Value("${config.jwt.expire:86400}")
private Long jwtExpire;
@Override
public QrLoginGenerateResponse generateQrLoginToken() {
// 生成唯一的扫码登录token
String token = UUID.randomUUID().toString(true);
// 创建扫码登录数据
QrLoginData qrLoginData = new QrLoginData();
qrLoginData.setToken(token);
qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING);
qrLoginData.setCreateTime(LocalDateTime.now());
qrLoginData.setExpireTime(LocalDateTime.now().plusSeconds(QR_LOGIN_TOKEN_TTL));
// 存储到Redis设置过期时间
String redisKey = QR_LOGIN_TOKEN_KEY + token;
redisUtil.set(redisKey, qrLoginData, QR_LOGIN_TOKEN_TTL, TimeUnit.SECONDS);
log.info("生成扫码登录token: {}", token);
// 构造二维码内容(这里可以是前端登录页面的URL + token参数)
String qrCodeContent = "qr-login:" + token;
return new QrLoginGenerateResponse(token, qrCodeContent, QR_LOGIN_TOKEN_TTL);
}
@Override
public QrLoginStatusResponse checkQrLoginStatus(String token) {
if (StrUtil.isBlank(token)) {
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
}
String redisKey = QR_LOGIN_TOKEN_KEY + token;
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
if (qrLoginData == null) {
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
}
// 检查是否过期
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
// 删除过期的token
redisUtil.delete(redisKey);
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
}
// 计算剩余过期时间
long expiresIn = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
QrLoginStatusResponse response = new QrLoginStatusResponse();
response.setStatus(qrLoginData.getStatus());
response.setExpiresIn(expiresIn);
// 如果已确认返回token和用户信息
if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus())) {
response.setAccessToken(qrLoginData.getAccessToken());
// 获取用户信息
if (qrLoginData.getUserId() != null) {
User user = userService.getByIdRel(qrLoginData.getUserId());
if (user != null) {
// 清除敏感信息
user.setPassword(null);
response.setUserInfo(user);
}
}
// 确认后删除token防止重复使用
redisUtil.delete(redisKey);
}
return response;
}
@Override
public QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request) {
String token = request.getToken();
Integer userId = request.getUserId();
String platform = request.getPlatform();
if (StrUtil.isBlank(token) || userId == null) {
throw new RuntimeException("参数不能为空");
}
String redisKey = QR_LOGIN_TOKEN_KEY + token;
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
if (qrLoginData == null) {
throw new RuntimeException("扫码登录token不存在或已过期");
}
// 检查是否过期
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
redisUtil.delete(redisKey);
throw new RuntimeException("扫码登录token已过期");
}
// 获取用户信息
User user = userService.getByIdRel(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
// 检查用户状态
if (user.getStatus() != null && user.getStatus() != 0) {
throw new RuntimeException("用户已被冻结");
}
// 如果是微信小程序登录,处理微信相关信息
if ("miniprogram".equals(platform) && request.getWechatInfo() != null) {
handleWechatMiniProgramLogin(user, request.getWechatInfo());
}
// 生成JWT token
JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId());
String accessToken = JwtUtil.buildToken(jwtSubject, jwtExpire, jwtSecret);
// 更新扫码登录数据
qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
qrLoginData.setUserId(userId);
qrLoginData.setUsername(user.getUsername());
qrLoginData.setAccessToken(accessToken);
// 更新Redis中的数据
redisUtil.set(redisKey, qrLoginData, 60L, TimeUnit.SECONDS); // 给前端60秒时间获取token
log.info("用户 {} 通过 {} 平台确认扫码登录token: {}", user.getUsername(),
platform != null ? platform : "unknown", token);
// 清除敏感信息
user.setPassword(null);
return new QrLoginStatusResponse(QR_LOGIN_STATUS_CONFIRMED, accessToken, user, 60L);
}
/**
* 处理微信小程序登录相关逻辑
*/
private void handleWechatMiniProgramLogin(User user, QrLoginConfirmRequest.WechatMiniProgramInfo wechatInfo) {
// 更新用户的微信信息
if (StrUtil.isNotBlank(wechatInfo.getOpenid())) {
user.setOpenid(wechatInfo.getOpenid());
}
if (StrUtil.isNotBlank(wechatInfo.getUnionid())) {
user.setUnionid(wechatInfo.getUnionid());
}
if (StrUtil.isNotBlank(wechatInfo.getNickname()) && StrUtil.isBlank(user.getNickname())) {
user.setNickname(wechatInfo.getNickname());
}
if (StrUtil.isNotBlank(wechatInfo.getAvatar()) && StrUtil.isBlank(user.getAvatar())) {
user.setAvatar(wechatInfo.getAvatar());
}
// 更新用户信息到数据库
try {
userService.updateById(user);
log.info("更新用户 {} 的微信小程序信息成功", user.getUsername());
} catch (Exception e) {
log.warn("更新用户 {} 的微信小程序信息失败: {}", user.getUsername(), e.getMessage());
}
}
@Override
public boolean scanQrCode(String token) {
if (StrUtil.isBlank(token)) {
return false;
}
String redisKey = QR_LOGIN_TOKEN_KEY + token;
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
if (qrLoginData == null) {
return false;
}
// 检查是否过期
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
redisUtil.delete(redisKey);
return false;
}
// 只有pending状态才能更新为scanned
if (QR_LOGIN_STATUS_PENDING.equals(qrLoginData.getStatus())) {
qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED);
// 计算剩余过期时间
long remainingSeconds = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
redisUtil.set(redisKey, qrLoginData, remainingSeconds, TimeUnit.SECONDS);
log.info("扫码登录token {} 状态更新为已扫码", token);
return true;
}
return false;
}
}

View File

@@ -1,122 +0,0 @@
package com.gxwebsoft.clinic.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.clinic.service.ClinicAppointmentService;
import com.gxwebsoft.clinic.entity.ClinicAppointment;
import com.gxwebsoft.clinic.param.ClinicAppointmentParam;
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 2025-10-19 09:27:04
*/
@Tag(name = "挂号管理")
@RestController
@RequestMapping("/api/clinic/clinic-appointment")
public class ClinicAppointmentController extends BaseController {
@Resource
private ClinicAppointmentService clinicAppointmentService;
@Operation(summary = "分页查询挂号")
@GetMapping("/page")
public ApiResult<PageResult<ClinicAppointment>> page(ClinicAppointmentParam param) {
// 使用关联查询
return success(clinicAppointmentService.pageRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicAppointment:list')")
@Operation(summary = "查询全部挂号")
@GetMapping()
public ApiResult<List<ClinicAppointment>> list(ClinicAppointmentParam param) {
// 使用关联查询
return success(clinicAppointmentService.listRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicAppointment:list')")
@Operation(summary = "根据id查询挂号")
@GetMapping("/{id}")
public ApiResult<ClinicAppointment> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(clinicAppointmentService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('clinic:clinicAppointment:save')")
@OperationLog
@Operation(summary = "添加挂号")
@PostMapping()
public ApiResult<?> save(@RequestBody ClinicAppointment clinicAppointment) {
if (clinicAppointmentService.save(clinicAppointment)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicAppointment:update')")
@OperationLog
@Operation(summary = "修改挂号")
@PutMapping()
public ApiResult<?> update(@RequestBody ClinicAppointment clinicAppointment) {
if (clinicAppointmentService.updateById(clinicAppointment)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicAppointment:remove')")
@OperationLog
@Operation(summary = "删除挂号")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (clinicAppointmentService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('clinic:clinicAppointment:save')")
@OperationLog
@Operation(summary = "批量添加挂号")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ClinicAppointment> list) {
if (clinicAppointmentService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicAppointment:update')")
@OperationLog
@Operation(summary = "批量修改挂号")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ClinicAppointment> batchParam) {
if (batchParam.update(clinicAppointmentService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicAppointment:remove')")
@OperationLog
@Operation(summary = "批量删除挂号")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (clinicAppointmentService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -1,128 +0,0 @@
package com.gxwebsoft.clinic.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.clinic.service.ClinicDoctorApplyService;
import com.gxwebsoft.clinic.entity.ClinicDoctorApply;
import com.gxwebsoft.clinic.param.ClinicDoctorApplyParam;
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 2025-10-19 09:27:04
*/
@Tag(name = "医生入驻申请管理")
@RestController
@RequestMapping("/api/clinic/clinic-doctor-apply")
public class ClinicDoctorApplyController extends BaseController {
@Resource
private ClinicDoctorApplyService clinicDoctorApplyService;
@PreAuthorize("hasAuthority('clinic:clinicDoctorApply:list')")
@Operation(summary = "分页查询医生入驻申请")
@GetMapping("/page")
public ApiResult<PageResult<ClinicDoctorApply>> page(ClinicDoctorApplyParam param) {
// 使用关联查询
return success(clinicDoctorApplyService.pageRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorApply:list')")
@Operation(summary = "查询全部医生入驻申请")
@GetMapping()
public ApiResult<List<ClinicDoctorApply>> list(ClinicDoctorApplyParam param) {
// 使用关联查询
return success(clinicDoctorApplyService.listRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorApply:list')")
@Operation(summary = "根据id查询医生入驻申请")
@GetMapping("/{id}")
public ApiResult<ClinicDoctorApply> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(clinicDoctorApplyService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorApply:save')")
@OperationLog
@Operation(summary = "添加医生入驻申请")
@PostMapping()
public ApiResult<?> save(@RequestBody ClinicDoctorApply clinicDoctorApply) {
// 记录当前登录用户id
// User loginUser = getLoginUser();
// if (loginUser != null) {
// clinicDoctorApply.setUserId(loginUser.getUserId());
// }
if (clinicDoctorApplyService.save(clinicDoctorApply)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorApply:update')")
@OperationLog
@Operation(summary = "修改医生入驻申请")
@PutMapping()
public ApiResult<?> update(@RequestBody ClinicDoctorApply clinicDoctorApply) {
if (clinicDoctorApplyService.updateById(clinicDoctorApply)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorApply:remove')")
@OperationLog
@Operation(summary = "删除医生入驻申请")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (clinicDoctorApplyService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorApply:save')")
@OperationLog
@Operation(summary = "批量添加医生入驻申请")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ClinicDoctorApply> list) {
if (clinicDoctorApplyService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorApply:update')")
@OperationLog
@Operation(summary = "批量修改医生入驻申请")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ClinicDoctorApply> batchParam) {
if (batchParam.update(clinicDoctorApplyService, "apply_id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorApply:remove')")
@OperationLog
@Operation(summary = "批量删除医生入驻申请")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (clinicDoctorApplyService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -1,128 +0,0 @@
package com.gxwebsoft.clinic.controller;
import com.gxwebsoft.clinic.entity.ClinicDoctorUser;
import com.gxwebsoft.clinic.param.ClinicDoctorUserParam;
import com.gxwebsoft.clinic.service.ClinicDoctorUserService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 分销商用户记录表控制器
*
* @author 科技小王子
* @since 2025-10-23 15:58:21
*/
@Tag(name = "分销商用户记录表管理")
@RestController
@RequestMapping("/api/clinic/clinic-doctor-user")
public class ClinicDoctorUserController extends BaseController {
@Resource
private ClinicDoctorUserService clinicDoctorUserService;
@PreAuthorize("hasAuthority('clinic:clinicDoctorUser:list')")
@Operation(summary = "分页查询分销商用户记录表")
@GetMapping("/page")
public ApiResult<PageResult<ClinicDoctorUser>> page(ClinicDoctorUserParam param) {
// 使用关联查询
return success(clinicDoctorUserService.pageRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorUser:list')")
@Operation(summary = "查询全部分销商用户记录表")
@GetMapping()
public ApiResult<List<ClinicDoctorUser>> list(ClinicDoctorUserParam param) {
// 使用关联查询
return success(clinicDoctorUserService.listRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorUser:list')")
@Operation(summary = "根据id查询分销商用户记录表")
@GetMapping("/{id}")
public ApiResult<ClinicDoctorUser> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(clinicDoctorUserService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorUser:save')")
@OperationLog
@Operation(summary = "添加分销商用户记录表")
@PostMapping()
public ApiResult<?> save(@RequestBody ClinicDoctorUser clinicDoctorUser) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
clinicDoctorUser.setUserId(loginUser.getUserId());
}
if (clinicDoctorUserService.save(clinicDoctorUser)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorUser:update')")
@OperationLog
@Operation(summary = "修改分销商用户记录表")
@PutMapping()
public ApiResult<?> update(@RequestBody ClinicDoctorUser clinicDoctorUser) {
if (clinicDoctorUserService.updateById(clinicDoctorUser)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorUser:remove')")
@OperationLog
@Operation(summary = "删除分销商用户记录表")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (clinicDoctorUserService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorUser:save')")
@OperationLog
@Operation(summary = "批量添加分销商用户记录表")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ClinicDoctorUser> list) {
if (clinicDoctorUserService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorUser:update')")
@OperationLog
@Operation(summary = "批量修改分销商用户记录表")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ClinicDoctorUser> batchParam) {
if (batchParam.update(clinicDoctorUserService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicDoctorUser:remove')")
@OperationLog
@Operation(summary = "批量删除分销商用户记录表")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (clinicDoctorUserService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -1,127 +0,0 @@
package com.gxwebsoft.clinic.controller;
import com.gxwebsoft.clinic.entity.ClinicMedicine;
import com.gxwebsoft.clinic.param.ClinicMedicineParam;
import com.gxwebsoft.clinic.service.ClinicMedicineService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
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 2025-10-22 02:06:32
*/
@Tag(name = "药品库管理")
@RestController
@RequestMapping("/api/clinic/clinic-medicine")
public class ClinicMedicineController extends BaseController {
@Resource
private ClinicMedicineService clinicMedicineService;
@PreAuthorize("hasAuthority('clinic:clinicMedicine:list')")
@Operation(summary = "分页查询药品库")
@GetMapping("/page")
public ApiResult<PageResult<ClinicMedicine>> page(ClinicMedicineParam param) {
// 使用关联查询
return success(clinicMedicineService.pageRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicMedicine:list')")
@Operation(summary = "查询全部药品库")
@GetMapping()
public ApiResult<List<ClinicMedicine>> list(ClinicMedicineParam param) {
// 使用关联查询
return success(clinicMedicineService.listRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicMedicine:list')")
@Operation(summary = "根据id查询药品库")
@GetMapping("/{id}")
public ApiResult<ClinicMedicine> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(clinicMedicineService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('clinic:clinicMedicine:save')")
@OperationLog
@Operation(summary = "添加药品库")
@PostMapping()
public ApiResult<?> save(@RequestBody ClinicMedicine clinicMedicine) {
// 记录当前登录用户id
// User loginUser = getLoginUser();
// if (loginUser != null) {
// clinicMedicine.setUserId(loginUser.getUserId());
// }
if (clinicMedicineService.save(clinicMedicine)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicine:update')")
@OperationLog
@Operation(summary = "修改药品库")
@PutMapping()
public ApiResult<?> update(@RequestBody ClinicMedicine clinicMedicine) {
if (clinicMedicineService.updateById(clinicMedicine)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicine:remove')")
@OperationLog
@Operation(summary = "删除药品库")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (clinicMedicineService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicine:save')")
@OperationLog
@Operation(summary = "批量添加药品库")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ClinicMedicine> list) {
if (clinicMedicineService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicine:update')")
@OperationLog
@Operation(summary = "批量修改药品库")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ClinicMedicine> batchParam) {
if (batchParam.update(clinicMedicineService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicine:remove')")
@OperationLog
@Operation(summary = "批量删除药品库")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (clinicMedicineService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -1,127 +0,0 @@
package com.gxwebsoft.clinic.controller;
import com.gxwebsoft.clinic.entity.ClinicMedicineInout;
import com.gxwebsoft.clinic.param.ClinicMedicineInoutParam;
import com.gxwebsoft.clinic.service.ClinicMedicineInoutService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
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 2025-10-22 02:06:32
*/
@Tag(name = "出入库管理")
@RestController
@RequestMapping("/api/clinic/clinic-medicine-inout")
public class ClinicMedicineInoutController extends BaseController {
@Resource
private ClinicMedicineInoutService clinicMedicineInoutService;
@PreAuthorize("hasAuthority('clinic:clinicMedicineInout:list')")
@Operation(summary = "分页查询出入库")
@GetMapping("/page")
public ApiResult<PageResult<ClinicMedicineInout>> page(ClinicMedicineInoutParam param) {
// 使用关联查询
return success(clinicMedicineInoutService.pageRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineInout:list')")
@Operation(summary = "查询全部出入库")
@GetMapping()
public ApiResult<List<ClinicMedicineInout>> list(ClinicMedicineInoutParam param) {
// 使用关联查询
return success(clinicMedicineInoutService.listRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineInout:list')")
@Operation(summary = "根据id查询出入库")
@GetMapping("/{id}")
public ApiResult<ClinicMedicineInout> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(clinicMedicineInoutService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineInout:save')")
@OperationLog
@Operation(summary = "添加出入库")
@PostMapping()
public ApiResult<?> save(@RequestBody ClinicMedicineInout clinicMedicineInout) {
// 记录当前登录用户id
// User loginUser = getLoginUser();
// if (loginUser != null) {
// clinicMedicineInout.setUserId(loginUser.getUserId());
// }
if (clinicMedicineInoutService.save(clinicMedicineInout)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineInout:update')")
@OperationLog
@Operation(summary = "修改出入库")
@PutMapping()
public ApiResult<?> update(@RequestBody ClinicMedicineInout clinicMedicineInout) {
if (clinicMedicineInoutService.updateById(clinicMedicineInout)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineInout:remove')")
@OperationLog
@Operation(summary = "删除出入库")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (clinicMedicineInoutService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineInout:save')")
@OperationLog
@Operation(summary = "批量添加出入库")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ClinicMedicineInout> list) {
if (clinicMedicineInoutService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineInout:update')")
@OperationLog
@Operation(summary = "批量修改出入库")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ClinicMedicineInout> batchParam) {
if (batchParam.update(clinicMedicineInoutService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineInout:remove')")
@OperationLog
@Operation(summary = "批量删除出入库")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (clinicMedicineInoutService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -1,127 +0,0 @@
package com.gxwebsoft.clinic.controller;
import com.gxwebsoft.clinic.entity.ClinicMedicineStock;
import com.gxwebsoft.clinic.param.ClinicMedicineStockParam;
import com.gxwebsoft.clinic.service.ClinicMedicineStockService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
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 2025-10-22 02:06:32
*/
@Tag(name = "药品库存管理")
@RestController
@RequestMapping("/api/clinic/clinic-medicine-stock")
public class ClinicMedicineStockController extends BaseController {
@Resource
private ClinicMedicineStockService clinicMedicineStockService;
@PreAuthorize("hasAuthority('clinic:clinicMedicineStock:list')")
@Operation(summary = "分页查询药品库存")
@GetMapping("/page")
public ApiResult<PageResult<ClinicMedicineStock>> page(ClinicMedicineStockParam param) {
// 使用关联查询
return success(clinicMedicineStockService.pageRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineStock:list')")
@Operation(summary = "查询全部药品库存")
@GetMapping()
public ApiResult<List<ClinicMedicineStock>> list(ClinicMedicineStockParam param) {
// 使用关联查询
return success(clinicMedicineStockService.listRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineStock:list')")
@Operation(summary = "根据id查询药品库存")
@GetMapping("/{id}")
public ApiResult<ClinicMedicineStock> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(clinicMedicineStockService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineStock:save')")
@OperationLog
@Operation(summary = "添加药品库存")
@PostMapping()
public ApiResult<?> save(@RequestBody ClinicMedicineStock clinicMedicineStock) {
// 记录当前登录用户id
// User loginUser = getLoginUser();
// if (loginUser != null) {
// clinicMedicineStock.setUserId(loginUser.getUserId());
// }
if (clinicMedicineStockService.save(clinicMedicineStock)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineStock:update')")
@OperationLog
@Operation(summary = "修改药品库存")
@PutMapping()
public ApiResult<?> update(@RequestBody ClinicMedicineStock clinicMedicineStock) {
if (clinicMedicineStockService.updateById(clinicMedicineStock)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineStock:remove')")
@OperationLog
@Operation(summary = "删除药品库存")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (clinicMedicineStockService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineStock:save')")
@OperationLog
@Operation(summary = "批量添加药品库存")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ClinicMedicineStock> list) {
if (clinicMedicineStockService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineStock:update')")
@OperationLog
@Operation(summary = "批量修改药品库存")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ClinicMedicineStock> batchParam) {
if (batchParam.update(clinicMedicineStockService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicMedicineStock:remove')")
@OperationLog
@Operation(summary = "批量删除药品库存")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (clinicMedicineStockService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -1,129 +0,0 @@
package com.gxwebsoft.clinic.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.clinic.service.ClinicPatientUserService;
import com.gxwebsoft.clinic.entity.ClinicPatientUser;
import com.gxwebsoft.clinic.param.ClinicPatientUserParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 患者控制器
*
* @author 科技小王子
* @since 2025-10-19 09:27:04
*/
@Tag(name = "患者管理")
@RestController
@RequestMapping("/api/clinic/clinic-patient-user")
public class ClinicPatientUserController extends BaseController {
@Resource
private ClinicPatientUserService clinicPatientUserService;
@PreAuthorize("hasAuthority('clinic:clinicPatientUser:list')")
@Operation(summary = "分页查询患者")
@GetMapping("/page")
public ApiResult<PageResult<ClinicPatientUser>> page(ClinicPatientUserParam param) {
// 使用关联查询
return success(clinicPatientUserService.pageRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicPatientUser:list')")
@Operation(summary = "查询全部患者")
@GetMapping()
public ApiResult<List<ClinicPatientUser>> list(ClinicPatientUserParam param) {
// 使用关联查询
return success(clinicPatientUserService.listRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicPatientUser:list')")
@Operation(summary = "根据id查询患者")
@GetMapping("/{id}")
public ApiResult<ClinicPatientUser> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(clinicPatientUserService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('clinic:clinicPatientUser:save')")
@OperationLog
@Operation(summary = "添加患者")
@PostMapping()
public ApiResult<?> save(@RequestBody ClinicPatientUser clinicPatientUser) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
clinicPatientUser.setUserId(loginUser.getUserId());
}
if (clinicPatientUserService.save(clinicPatientUser)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPatientUser:update')")
@OperationLog
@Operation(summary = "修改患者")
@PutMapping()
public ApiResult<?> update(@RequestBody ClinicPatientUser clinicPatientUser) {
if (clinicPatientUserService.updateById(clinicPatientUser)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPatientUser:remove')")
@OperationLog
@Operation(summary = "删除患者")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (clinicPatientUserService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPatientUser:save')")
@OperationLog
@Operation(summary = "批量添加患者")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ClinicPatientUser> list) {
if (clinicPatientUserService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPatientUser:update')")
@OperationLog
@Operation(summary = "批量修改患者")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ClinicPatientUser> batchParam) {
if (batchParam.update(clinicPatientUserService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPatientUser:remove')")
@OperationLog
@Operation(summary = "批量删除患者")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (clinicPatientUserService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -1,191 +0,0 @@
package com.gxwebsoft.clinic.controller;
import cn.hutool.core.util.IdUtil;
import com.gxwebsoft.clinic.dto.PrescriptionOrderRequest;
import com.gxwebsoft.clinic.entity.ClinicPrescription;
import com.gxwebsoft.clinic.param.ClinicPrescriptionParam;
import com.gxwebsoft.clinic.service.ClinicPrescriptionService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
/**
* 处方主表
控制器
*
* @author 科技小王子
* @since 2025-10-22 02:01:13
*/
@Tag(name = "处方主表管理")
@RestController
@RequestMapping("/api/clinic/clinic-prescription")
public class ClinicPrescriptionController extends BaseController {
@Resource
private ClinicPrescriptionService clinicPrescriptionService;
@Operation(summary = "分页查询处方主表")
@GetMapping("/page")
public ApiResult<PageResult<ClinicPrescription>> page(ClinicPrescriptionParam param) {
// 使用关联查询
return success(clinicPrescriptionService.pageRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:list')")
@Operation(summary = "查询全部处方主表")
@GetMapping()
public ApiResult<List<ClinicPrescription>> list(ClinicPrescriptionParam param) {
// 使用关联查询
return success(clinicPrescriptionService.listRel(param));
}
@Operation(summary = "根据id查询处方主表")
@GetMapping("/{id}")
public ApiResult<ClinicPrescription> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(clinicPrescriptionService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:save')")
@OperationLog
@Operation(summary = "添加处方主表")
@PostMapping()
public ApiResult<?> save(@RequestBody ClinicPrescription clinicPrescription) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
clinicPrescription.setDoctorId(loginUser.getUserId());
// 生成订单号
String orderNo = Long.toString(IdUtil.getSnowflakeNextId());
clinicPrescription.setOrderNo(orderNo);
}
if (clinicPrescriptionService.save(clinicPrescription)) {
// 返回处方数据包含处方ID
return success("添加成功",clinicPrescriptionService.getByLastId(clinicPrescription));
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:update')")
@OperationLog
@Operation(summary = "修改处方主表")
@PutMapping()
public ApiResult<?> update(@RequestBody ClinicPrescription clinicPrescription) {
if (clinicPrescriptionService.updateById(clinicPrescription)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:remove')")
@OperationLog
@Operation(summary = "删除处方主表")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (clinicPrescriptionService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:save')")
@OperationLog
@Operation(summary = "批量添加处方主表")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ClinicPrescription> list) {
if (clinicPrescriptionService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:update')")
@OperationLog
@Operation(summary = "批量修改处方主表")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ClinicPrescription> batchParam) {
if (batchParam.update(clinicPrescriptionService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:remove')")
@OperationLog
@Operation(summary = "批量删除处方主表")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (clinicPrescriptionService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
@Operation(summary = "创建处方订单")
@PostMapping("/order")
public ApiResult<?> createOrder(@RequestBody PrescriptionOrderRequest request) {
try {
// 1. 参数校验
if (request.getPrescriptionId() == null) {
return fail("处方ID不能为空");
}
if (request.getPayType() == null) {
return fail("支付方式不能为空");
}
// 2. 查询处方信息
ClinicPrescription prescription = clinicPrescriptionService.getById(request.getPrescriptionId());
if (prescription == null) {
return fail("处方不存在");
}
// 3. 检查处方状态
if (prescription.getStatus() != null && prescription.getStatus() == 2) {
return fail("该处方已支付,无需重复支付");
}
if (prescription.getStatus() != null && prescription.getStatus() == 3) {
return fail("该处方已取消,无法支付");
}
// 4. 更新处方订单信息
ClinicPrescription updatePrescription = new ClinicPrescription();
updatePrescription.setId(request.getPrescriptionId());
// 根据支付类型更新状态
if (request.getPayType() == 1) {
// 微信支付,状态保持为正常,等待支付回调
updatePrescription.setStatus(0);
} else if (request.getPayType() == 4 || request.getPayType() == 5) {
// 现金支付或POS机支付直接标记为已支付
updatePrescription.setStatus(2);
updatePrescription.setIsSettled(1);
updatePrescription.setSettleTime(LocalDateTime.now());
} else if (request.getPayType() == 6) {
// 免费,直接标记为已完成
updatePrescription.setStatus(1);
updatePrescription.setIsSettled(1);
updatePrescription.setSettleTime(LocalDateTime.now());
}
if (clinicPrescriptionService.updateById(updatePrescription)) {
return success("订单创建成功", prescription);
}
return fail("订单创建失败");
} catch (Exception e) {
return fail("订单创建失败:" + e.getMessage());
}
}
}

View File

@@ -1,121 +0,0 @@
package com.gxwebsoft.clinic.controller;
import com.gxwebsoft.clinic.entity.ClinicPrescriptionItem;
import com.gxwebsoft.clinic.param.ClinicPrescriptionItemParam;
import com.gxwebsoft.clinic.service.ClinicPrescriptionItemService;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
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 2025-10-22 02:01:13
*/
@Tag(name = "处方明细表管理")
@RestController
@RequestMapping("/api/clinic/clinic-prescription-item")
public class ClinicPrescriptionItemController extends BaseController {
@Resource
private ClinicPrescriptionItemService clinicPrescriptionItemService;
@Operation(summary = "分页查询处方明细表")
@GetMapping("/page")
public ApiResult<PageResult<ClinicPrescriptionItem>> page(ClinicPrescriptionItemParam param) {
// 使用关联查询
return success(clinicPrescriptionItemService.pageRel(param));
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:list')")
@Operation(summary = "查询全部处方明细表")
@GetMapping()
public ApiResult<List<ClinicPrescriptionItem>> list(ClinicPrescriptionItemParam param) {
// 使用关联查询
return success(clinicPrescriptionItemService.listRel(param));
}
@Operation(summary = "根据id查询处方明细表")
@GetMapping("/{id}")
public ApiResult<ClinicPrescriptionItem> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(clinicPrescriptionItemService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:save')")
@OperationLog
@Operation(summary = "添加处方明细表")
@PostMapping()
public ApiResult<?> save(@RequestBody ClinicPrescriptionItem clinicPrescriptionItem) {
if (clinicPrescriptionItemService.save(clinicPrescriptionItem)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:update')")
@OperationLog
@Operation(summary = "修改处方明细表")
@PutMapping()
public ApiResult<?> update(@RequestBody ClinicPrescriptionItem clinicPrescriptionItem) {
if (clinicPrescriptionItemService.updateById(clinicPrescriptionItem)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:remove')")
@OperationLog
@Operation(summary = "删除处方明细表")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (clinicPrescriptionItemService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:save')")
@OperationLog
@Operation(summary = "批量添加处方明细表")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ClinicPrescriptionItem> list) {
if (clinicPrescriptionItemService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:update')")
@OperationLog
@Operation(summary = "批量修改处方明细表")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ClinicPrescriptionItem> batchParam) {
if (batchParam.update(clinicPrescriptionItemService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('clinic:clinicPrescription:remove')")
@OperationLog
@Operation(summary = "批量删除处方明细表")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (clinicPrescriptionItemService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -1,24 +0,0 @@
package com.gxwebsoft.clinic.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 处方订单请求参数
*
* @author 科技小王子
* @since 2025-11-03
*/
@Data
@Schema(name = "PrescriptionOrderRequest", description = "处方订单请求参数")
public class PrescriptionOrderRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "处方ID", required = true)
private Integer prescriptionId;
@Schema(description = "支付方式0余额支付1微信支付2支付宝支付3银联支付4现金支付5POS机支付6免费7积分支付", required = true)
private Integer payType;
}

View File

@@ -1,125 +0,0 @@
package com.gxwebsoft.clinic.entity;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType;
import java.time.LocalDate;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
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 2025-10-19 09:27:04
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "ClinicDoctorApply对象", description = "医生入驻申请")
public class ClinicDoctorApply implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "apply_id", type = IdType.AUTO)
private Integer applyId;
@Schema(description = "类型 0医生")
private Integer type;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "姓名")
private String realName;
@Schema(description = "性别 1男 2女")
private Integer gender;
@Schema(description = "手机号")
private String mobile;
@Schema(description = "客户名称")
private String dealerName;
@Schema(description = "证件号码")
private String idCard;
@Schema(description = "生日")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate birthDate;
@Schema(description = "区分职称等级(如主治医师、副主任医师)")
private String professionalTitle;
@Schema(description = "工作单位")
private String workUnit;
@Schema(description = "执业资格核心凭证")
private String practiceLicense;
@Schema(description = "限定可执业科室或疾病类型")
private String practiceScope;
@Schema(description = "开始工作时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startWorkDate;
@Schema(description = "简历")
private String resume;
@Schema(description = "使用 JSON 存储多个证件文件路径(如执业证、学历证)")
private String certificationFiles;
@Schema(description = "详细地址")
private String address;
@Schema(description = "签约价格")
private BigDecimal money;
@Schema(description = "推荐人用户ID")
private Integer refereeId;
@Schema(description = "申请方式(10需后台审核 20无需审核)")
private Integer applyType;
@Schema(description = "审核状态 (10待审核 20审核通过 30驳回)")
private Integer applyStatus;
@Schema(description = "申请时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime applyTime;
@Schema(description = "审核时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime auditTime;
@Schema(description = "合同时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime contractTime;
@Schema(description = "过期时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime expirationTime;
@Schema(description = "驳回原因")
private String rejectReason;
@Schema(description = "备注")
private String comments;
@Schema(description = "商城ID")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -1,99 +0,0 @@
package com.gxwebsoft.clinic.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 分销商用户记录表
*
* @author 科技小王子
* @since 2025-10-23 15:58:20
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "ClinicDoctorUser对象", description = "分销商用户记录表")
public class ClinicDoctorUser 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 = "自增ID")
private Integer userId;
@Schema(description = "昵称")
@TableField(exist = false)
private String nickname;
@Schema(description = "头像")
@TableField(exist = false)
private String avatar;
@Schema(description = "姓名")
private String realName;
@Schema(description = "手机号")
@TableField(exist = false)
private String phone;
@Schema(description = "部门")
private Integer departmentId;
@Schema(description = "专业领域")
private String specialty;
@Schema(description = "职务级别")
private String position;
@Schema(description = "执业资格")
private String qualification;
@Schema(description = "医生简介")
private String introduction;
@Schema(description = "挂号费")
private BigDecimal consultationFee;
@Schema(description = "工作年限")
private Integer workYears;
@Schema(description = "问诊人数")
private Integer consultationCount;
@Schema(description = "专属二维码")
private String qrcode;
@Schema(description = "备注")
private String comments;
@Schema(description = "排序号")
private Integer sortNumber;
@Schema(description = "是否删除")
private Integer isDelete;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -1,71 +0,0 @@
package com.gxwebsoft.clinic.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 药品库
*
* @author 科技小王子
* @since 2025-10-22 02:06:31
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "ClinicMedicine对象", description = "药品库")
public class ClinicMedicine implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "药名")
private String name;
@Schema(description = "拼音")
private String pinyin;
@Schema(description = "分类(如“清热解毒”、“补气养血”)")
private String category;
@Schema(description = "规格(如“饮片”、“颗粒”)")
private String specification;
@Schema(description = "单位(如“克”、“袋”)")
private String unit;
@Schema(description = "描述")
private String content;
@Schema(description = "单价")
private BigDecimal pricePerUnit;
@Schema(description = "是否活跃")
private Integer isActive;
@Schema(description = "买家用户ID")
private Integer userId;
@Schema(description = "备注")
private String comments;
@Schema(description = "商城ID")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -1,99 +0,0 @@
package com.gxwebsoft.clinic.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 出入库
*
* @author 科技小王子
* @since 2025-10-22 02:06:32
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "ClinicMedicineInout对象", description = "出入库")
public class ClinicMedicineInout implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "买家用户ID")
private Integer userId;
@Schema(description = "订单编号")
private String orderNo;
@Schema(description = "分销商用户id(一级)")
private Integer firstUserId;
@Schema(description = "分销商用户id(二级)")
private Integer secondUserId;
@Schema(description = "分销商用户id(三级)")
private Integer thirdUserId;
@Schema(description = "分销佣金(一级)")
private BigDecimal firstMoney;
@Schema(description = "分销佣金(二级)")
private BigDecimal secondMoney;
@Schema(description = "分销佣金(三级)")
private BigDecimal thirdMoney;
@Schema(description = "单价")
private BigDecimal price;
@Schema(description = "订单总金额")
private BigDecimal orderPrice;
@Schema(description = "结算金额")
private BigDecimal settledPrice;
@Schema(description = "换算成度")
private BigDecimal degreePrice;
@Schema(description = "实发金额")
private BigDecimal payPrice;
@Schema(description = "税率")
private BigDecimal rate;
@Schema(description = "结算月份")
private String month;
@Schema(description = "订单是否失效(0未失效 1已失效)")
private Integer isInvalid;
@Schema(description = "佣金结算(0未结算 1已结算)")
private Integer isSettled;
@Schema(description = "结算时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime settleTime;
@Schema(description = "备注")
private String comments;
@Schema(description = "商城ID")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -1,133 +0,0 @@
package com.gxwebsoft.clinic.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.gxwebsoft.shop.entity.ShopOrder;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 处方主表
*
* @author 科技小王子
* @since 2025-10-22 02:01:13
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "ClinicPrescription对象", description = "处方主表")
public class ClinicPrescription implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "患者")
private Integer userId;
@Schema(description = "患者名称")
@TableField(exist = false)
private String realName;
@Schema(description = "年龄")
@TableField(exist = false)
private String age;
@Schema(description = "身高")
@TableField(exist = false)
private String height;
@Schema(description = "体重")
@TableField(exist = false)
private String weight;
@Schema(description = "医生")
private Integer doctorId;
@Schema(description = "医生名称")
@TableField(exist = false)
private String doctorName;
@Schema(description = "医生资格")
@TableField(exist = false)
private String qualification;
@Schema(description = "订单编号")
private String orderNo;
@Schema(description = "0未付款1已付款")
@TableField(exist = false)
private Boolean payStatus;
@Schema(description = "0未使用1已完成2已取消3取消中4退款申请中5退款被拒绝6退款成功7客户端申请退款")
@TableField(exist = false)
private Integer orderStatus;
@Schema(description = "关联就诊表")
private Integer visitRecordId;
@Schema(description = "处方类型 0中药 1西药")
private Integer prescriptionType;
@Schema(description = "诊断结果")
private String diagnosis;
@Schema(description = "治疗方案")
private String treatmentPlan;
@Schema(description = "煎药说明")
private String decoctionInstructions;
@Schema(description = "上传附件")
private String image;
@Schema(description = "订单总金额")
private BigDecimal orderPrice;
@Schema(description = "单价")
private BigDecimal price;
@Schema(description = "实付金额")
private BigDecimal payPrice;
@Schema(description = "订单是否失效(0未失效 1已失效)")
private Integer isInvalid;
@Schema(description = "结算(0未结算 1已结算)")
private Integer isSettled;
@Schema(description = "结算时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime settleTime;
@Schema(description = "状态, 0正常, 1已完成2已支付3已取消")
private Integer status;
@Schema(description = "备注")
private String comments;
@Schema(description = "商城ID")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "处方明细")
@TableField(exist = false)
private List<ClinicPrescriptionItem> items;
}

View File

@@ -1,99 +0,0 @@
package com.gxwebsoft.clinic.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 处方明细表
*
* @author 科技小王子
* @since 2025-10-22 02:01:13
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "ClinicPrescriptionItem对象", description = "处方明细表")
public class ClinicPrescriptionItem implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "自增ID")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "关联处方")
private Integer prescriptionId;
@Schema(description = "订单编号")
private String prescriptionNo;
@Schema(description = "关联药品")
private Integer medicineId;
@Schema(description = "药品名称")
@TableField(exist = false)
private String medicineName;
@Schema(description = "规格")
@TableField(exist = false)
private String specification;
@Schema(description = "单位")
@TableField(exist = false)
private String unit;
@Schema(description = "单价")
@TableField(exist = false)
private BigDecimal pricePerUnit;
@Schema(description = "药品")
@TableField(exist = false)
private ClinicMedicine clinicMedicine;
@Schema(description = "剂量如“10g”")
private String dosage;
@Schema(description = "用法频率(如“每日三次”)")
private String usageFrequency;
@Schema(description = "服用天数")
private Integer days;
@Schema(description = "购买数量")
private Integer amount;
@Schema(description = "单价")
private BigDecimal unitPrice;
@Schema(description = "数量")
private Integer quantity;
@Schema(description = "排序号")
private Integer sortNumber;
@Schema(description = "备注")
private String comments;
@Schema(description = "用户id")
private Integer userId;
@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

@@ -1,37 +0,0 @@
package com.gxwebsoft.clinic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.clinic.entity.ClinicAppointment;
import com.gxwebsoft.clinic.param.ClinicAppointmentParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 挂号Mapper
*
* @author 科技小王子
* @since 2025-10-19 09:27:03
*/
public interface ClinicAppointmentMapper extends BaseMapper<ClinicAppointment> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ClinicAppointment>
*/
List<ClinicAppointment> selectPageRel(@Param("page") IPage<ClinicAppointment> page,
@Param("param") ClinicAppointmentParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ClinicAppointment> selectListRel(@Param("param") ClinicAppointmentParam param);
}

View File

@@ -1,37 +0,0 @@
package com.gxwebsoft.clinic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.clinic.entity.ClinicDoctorApply;
import com.gxwebsoft.clinic.param.ClinicDoctorApplyParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 医生入驻申请Mapper
*
* @author 科技小王子
* @since 2025-10-19 09:27:04
*/
public interface ClinicDoctorApplyMapper extends BaseMapper<ClinicDoctorApply> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ClinicDoctorApply>
*/
List<ClinicDoctorApply> selectPageRel(@Param("page") IPage<ClinicDoctorApply> page,
@Param("param") ClinicDoctorApplyParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ClinicDoctorApply> selectListRel(@Param("param") ClinicDoctorApplyParam param);
}

View File

@@ -1,37 +0,0 @@
package com.gxwebsoft.clinic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.clinic.entity.ClinicDoctorUser;
import com.gxwebsoft.clinic.param.ClinicDoctorUserParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 分销商用户记录表Mapper
*
* @author 科技小王子
* @since 2025-10-19 09:27:04
*/
public interface ClinicDoctorUserMapper extends BaseMapper<ClinicDoctorUser> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ClinicDoctorUser>
*/
List<ClinicDoctorUser> selectPageRel(@Param("page") IPage<ClinicDoctorUser> page,
@Param("param") ClinicDoctorUserParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ClinicDoctorUser> selectListRel(@Param("param") ClinicDoctorUserParam param);
}

View File

@@ -1,37 +0,0 @@
package com.gxwebsoft.clinic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.clinic.entity.ClinicMedicineInout;
import com.gxwebsoft.clinic.param.ClinicMedicineInoutParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 出入库Mapper
*
* @author 科技小王子
* @since 2025-10-22 02:06:32
*/
public interface ClinicMedicineInoutMapper extends BaseMapper<ClinicMedicineInout> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ClinicMedicineInout>
*/
List<ClinicMedicineInout> selectPageRel(@Param("page") IPage<ClinicMedicineInout> page,
@Param("param") ClinicMedicineInoutParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ClinicMedicineInout> selectListRel(@Param("param") ClinicMedicineInoutParam param);
}

View File

@@ -1,37 +0,0 @@
package com.gxwebsoft.clinic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.clinic.entity.ClinicMedicine;
import com.gxwebsoft.clinic.param.ClinicMedicineParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 药品库Mapper
*
* @author 科技小王子
* @since 2025-10-22 02:06:31
*/
public interface ClinicMedicineMapper extends BaseMapper<ClinicMedicine> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ClinicMedicine>
*/
List<ClinicMedicine> selectPageRel(@Param("page") IPage<ClinicMedicine> page,
@Param("param") ClinicMedicineParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ClinicMedicine> selectListRel(@Param("param") ClinicMedicineParam param);
}

View File

@@ -1,37 +0,0 @@
package com.gxwebsoft.clinic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.clinic.entity.ClinicMedicineStock;
import com.gxwebsoft.clinic.param.ClinicMedicineStockParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 药品库存Mapper
*
* @author 科技小王子
* @since 2025-10-22 02:06:32
*/
public interface ClinicMedicineStockMapper extends BaseMapper<ClinicMedicineStock> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ClinicMedicineStock>
*/
List<ClinicMedicineStock> selectPageRel(@Param("page") IPage<ClinicMedicineStock> page,
@Param("param") ClinicMedicineStockParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ClinicMedicineStock> selectListRel(@Param("param") ClinicMedicineStockParam param);
}

View File

@@ -1,37 +0,0 @@
package com.gxwebsoft.clinic.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.clinic.entity.ClinicPatientUser;
import com.gxwebsoft.clinic.param.ClinicPatientUserParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 患者Mapper
*
* @author 科技小王子
* @since 2025-10-19 09:27:04
*/
public interface ClinicPatientUserMapper extends BaseMapper<ClinicPatientUser> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ClinicPatientUser>
*/
List<ClinicPatientUser> selectPageRel(@Param("page") IPage<ClinicPatientUser> page,
@Param("param") ClinicPatientUserParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ClinicPatientUser> selectListRel(@Param("param") ClinicPatientUserParam param);
}

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