feat: 添加ERP服务和系统服务,完善员工门户功能
## 新增服务模块 ### 1. ERP服务 (hzhub-erp) - 新增独立的ERP数据适配服务 - 支持SQL Server 2008 R2数据源 - 提供动态API配置管理系统 - 包含客户管理、销售数据等业务接口 ### 2. 系统服务 (hzhub-system) - 新增独立的系统管理服务 - 用户、角色、权限、部门、菜单管理 - 租户管理、操作日志、在线用户监控 - 工作流引擎(warm-flow)集成 - 企业微信审批同步功能 ### 3. API网关 (hzhub-gateway) - 新增Spring Cloud Gateway网关服务 - JWT认证、路由分发、限流熔断 - XSS防护、请求日志记录 - 统一入口端口8080 ## 后台管理功能增强 ### ERP动态API管理 - 新增动态API配置管理界面 - API测试、文档预览、统计监控 - 错误日志查看、缓存管理 - 从数据库表自动导入API配置 ### 系统管理增强 - 企业微信配置管理 - 企业微信审批同步配置 - 部门和用户管理优化 ## 员工门户功能完善 ### 业务页面 - 审批中心:工作流审批、待办任务 - CRM管理:客户关系管理 - 经销商管理:经销商数据展示 - 供应链管理:采购、库存、销售 - BI报表:数据可视化分析 - ERP数据探索:SQL Server数据查询 ### 个人中心 - 基本设置:个人信息管理 - 安全设置:密码修改、登录日志 - 锁屏功能:自动锁屏、手动锁屏 ### 其他功能 - 标签页管理:多标签页导航 - 页面缓存:keepAlive缓存机制 - 会话超时:自动检测并提示 ## 经销商门户 ### 页面路由 - 新增经销商管理页面路由 - AI聊天界面完善 ## 文档更新 - ERP API数据库初始化指南 - ERP API前端完整实现文档 - ERP API测试和验证指南 - Gateway路由迁移计划 - 项目配置文档更新 ## 部署脚本 - 统一启动/停止/重启脚本 - Docker Compose配置优化 - Nginx配置文件更新 ## 技术栈 - 后端: Spring Boot 3.5.8, Java 17 - 前端: Vue 3, TypeScript, Element Plus, Vben Admin - 工作流: warm-flow 1.8.2 - 网关: Spring Cloud Gateway - 数据库: MySQL 8.0, SQL Server 2008 R2 - 缓存: Redis 7 - 向量库: Weaviate 1.25.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
package com.foshanhuiya.erp;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* HZHub ERP服务启动类
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class HzhubErpApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HzhubErpApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.hzhub.erp;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* HZHub ERP服务启动类
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@SpringBootApplication(excludeName = {
|
||||
"cn.dev33.satoken.spring.SaTokenContextRegister" // 完全排除 Sa-Token 自动配置(开发阶段)
|
||||
}) // 开发阶段完全禁用 Sa-Token,生产环境需要移除此排除并启用认证
|
||||
@MapperScan("org.hzhub.erp.mapper")
|
||||
public class HzhubErpApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HzhubErpApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.hzhub.erp.common.core;
|
||||
|
||||
import org.hzhub.erp.common.domain.R;
|
||||
import org.hzhub.erp.common.page.TableDataInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 控制器基类
|
||||
*/
|
||||
public class BaseController {
|
||||
|
||||
/**
|
||||
* 响应返回结果
|
||||
*/
|
||||
protected <T> R<T> toAjax(int rows) {
|
||||
return rows > 0 ? R.ok() : R.fail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应返回结果
|
||||
*/
|
||||
protected <T> R<T> toAjax(boolean result) {
|
||||
return result ? R.ok() : R.fail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面跳转
|
||||
*/
|
||||
protected String redirect(String url) {
|
||||
return String.format("redirect:%s", url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分页数据
|
||||
*/
|
||||
protected <T> TableDataInfo<T> getDataTable(List<T> list) {
|
||||
return new TableDataInfo<>(list, list.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.hzhub.erp.common.core;
|
||||
|
||||
/**
|
||||
* HTTP状态码常量
|
||||
*/
|
||||
public final class HttpStatus {
|
||||
|
||||
private HttpStatus() {}
|
||||
|
||||
/**
|
||||
* 操作成功
|
||||
*/
|
||||
public static final int SUCCESS = 200;
|
||||
|
||||
/**
|
||||
* 对象创建成功
|
||||
*/
|
||||
public static final int CREATED = 201;
|
||||
|
||||
/**
|
||||
* 请求已经被接受
|
||||
*/
|
||||
public static final int ACCEPTED = 202;
|
||||
|
||||
/**
|
||||
* 操作已经执行成功,但是没有返回数据
|
||||
*/
|
||||
public static final int NO_CONTENT = 204;
|
||||
|
||||
/**
|
||||
* 资源未找到
|
||||
*/
|
||||
public static final int NOT_FOUND = 404;
|
||||
|
||||
/**
|
||||
* 请求方法未允许
|
||||
*/
|
||||
public static final int METHOD_NOT_ALLOWED = 405;
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
*/
|
||||
public static final int WARN = 601;
|
||||
|
||||
/**
|
||||
* 操作失败
|
||||
*/
|
||||
public static final int ERROR = 500;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.hzhub.erp.common.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Entity基类
|
||||
*/
|
||||
@Data
|
||||
public class BaseEntity implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 搜索值
|
||||
*/
|
||||
@JsonIgnore
|
||||
@TableField(exist = false)
|
||||
private String searchValue;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@TableField(exist = false)
|
||||
private Map<String, Object> params = new HashMap<>();
|
||||
}
|
||||
85
hzhub-erp/src/main/java/org/hzhub/erp/common/domain/R.java
Normal file
85
hzhub-erp/src/main/java/org/hzhub/erp/common/domain/R.java
Normal file
@@ -0,0 +1,85 @@
|
||||
package org.hzhub.erp.common.domain;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 响应信息主体
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class R<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final int SUCCESS = 200;
|
||||
public static final int FAIL = 500;
|
||||
|
||||
private int code;
|
||||
private String msg;
|
||||
private T data;
|
||||
|
||||
public static <T> R<T> ok() {
|
||||
return restResult(null, SUCCESS, "操作成功");
|
||||
}
|
||||
|
||||
public static <T> R<T> ok(T data) {
|
||||
return restResult(data, SUCCESS, "操作成功");
|
||||
}
|
||||
|
||||
public static <T> R<T> ok(String msg) {
|
||||
return restResult(null, SUCCESS, msg);
|
||||
}
|
||||
|
||||
public static <T> R<T> ok(String msg, T data) {
|
||||
return restResult(data, SUCCESS, msg);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail() {
|
||||
return restResult(null, FAIL, "操作失败");
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(String msg) {
|
||||
return restResult(null, FAIL, msg);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(T data) {
|
||||
return restResult(data, FAIL, "操作失败");
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(String msg, T data) {
|
||||
return restResult(data, FAIL, msg);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(int code, String msg) {
|
||||
return restResult(null, code, msg);
|
||||
}
|
||||
|
||||
public static <T> R<T> warn(String msg) {
|
||||
return restResult(null, 601, msg);
|
||||
}
|
||||
|
||||
public static <T> R<T> warn(String msg, T data) {
|
||||
return restResult(data, 601, msg);
|
||||
}
|
||||
|
||||
private static <T> R<T> restResult(T data, int code, String msg) {
|
||||
R<T> r = new R<>();
|
||||
r.setCode(code);
|
||||
r.setData(data);
|
||||
r.setMsg(msg);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static <T> Boolean isError(R<T> ret) {
|
||||
return !isSuccess(ret);
|
||||
}
|
||||
|
||||
public static <T> Boolean isSuccess(R<T> ret) {
|
||||
return R.SUCCESS == ret.getCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.hzhub.erp.common.page;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 表格分页数据对象
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class TableDataInfo<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
private long total;
|
||||
|
||||
/**
|
||||
* 列表数据
|
||||
*/
|
||||
private List<T> rows;
|
||||
|
||||
/**
|
||||
* 消息状态码
|
||||
*/
|
||||
private int code;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
public TableDataInfo(List<T> list, long total) {
|
||||
this.rows = list;
|
||||
this.total = total;
|
||||
this.code = 200;
|
||||
this.msg = "查询成功";
|
||||
}
|
||||
|
||||
public static <T> TableDataInfo<T> build(List<T> list) {
|
||||
TableDataInfo<T> rspData = new TableDataInfo<>();
|
||||
rspData.setCode(200);
|
||||
rspData.setMsg("查询成功");
|
||||
rspData.setRows(list);
|
||||
rspData.setTotal(list.size());
|
||||
return rspData;
|
||||
}
|
||||
|
||||
public static <T> TableDataInfo<T> build() {
|
||||
TableDataInfo<T> rspData = new TableDataInfo<>();
|
||||
rspData.setCode(200);
|
||||
rspData.setMsg("查询成功");
|
||||
return rspData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.hzhub.erp.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 拦截器
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 分页插件 - MySQL用于配置管理,SQL Server查询使用DynamicApiExecutor(已自定义分页)
|
||||
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
|
||||
// 设置最大单页限制数量,-1不受限制
|
||||
paginationInterceptor.setMaxLimit(500L);
|
||||
interceptor.addInnerInterceptor(paginationInterceptor);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动填充处理器
|
||||
*/
|
||||
@Bean
|
||||
public MetaObjectHandler metaObjectHandler() {
|
||||
return new MetaObjectHandler() {
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
|
||||
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.hzhub.erp.config;
|
||||
|
||||
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
|
||||
import cn.dev33.satoken.stp.StpLogic;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Sa-Token 配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfig {
|
||||
|
||||
/**
|
||||
* 使用 Simple JWT 模式,与 hzhub-ai 保持一致
|
||||
*/
|
||||
@Bean
|
||||
public StpLogic getStpLogicJwt() {
|
||||
return new StpLogicJwtForSimple();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.hzhub.erp.config;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.exception.NotPermissionException;
|
||||
import org.hzhub.erp.common.domain.R;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* Sa-Token 异常处理器
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class SaTokenExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SaTokenExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
public R<Void> handleNotLoginException(NotLoginException e) {
|
||||
log.error("未登录: {}", e.getMessage());
|
||||
return R.fail(401, "未登录或登录已过期");
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotPermissionException.class)
|
||||
public R<Void> handleNotPermissionException(NotPermissionException e) {
|
||||
log.error("无权限: {}", e.getMessage());
|
||||
return R.fail(403, "没有访问权限,请联系管理员授权");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.hzhub.erp.config;
|
||||
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Sa-Token 路由拦截器配置
|
||||
*/
|
||||
@Configuration
|
||||
public class SecurityConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 不需要拦截的路径
|
||||
*/
|
||||
private static final List<String> EXCLUDE_PATHS = Arrays.asList(
|
||||
"/erp/test/**",
|
||||
"/erp/customer/**",
|
||||
"/erp/api/**", // API配置管理(开发阶段暂时跳过认证)
|
||||
"/erp/dynamic/**", // 动态API执行(开发阶段暂时跳过认证)
|
||||
"/actuator/**",
|
||||
"/error"
|
||||
);
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 开发阶段:完全跳过拦截器配置,不添加任何拦截器
|
||||
// 生产环境:需要启用以下拦截器配置
|
||||
|
||||
/*
|
||||
registry.addInterceptor(new SaInterceptor(handle -> {
|
||||
// 如果请求来自 Gateway(已验证 JWT),直接放行
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
if ("true".equals(request.getHeader("X-Gateway-Verified"))) {
|
||||
return;
|
||||
}
|
||||
StpUtil.checkLogin();
|
||||
}))
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(EXCLUDE_PATHS);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.hzhub.erp.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.hzhub.erp.common.core.BaseController;
|
||||
import org.hzhub.erp.common.domain.R;
|
||||
import org.hzhub.erp.common.page.TableDataInfo;
|
||||
import org.hzhub.erp.domain.vo.CustomerVO;
|
||||
import org.hzhub.erp.service.ICustomerService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 客户档案 API(已废弃)
|
||||
*
|
||||
* @deprecated 已迁移到动态API系统,请使用:
|
||||
* - /erp/dynamic/v1/customer/list 替代 /erp/customer/list
|
||||
* - /erp/dynamic/v1/customer/detail 替代 /erp/customer/{customerCode}
|
||||
* - /erp/dynamic/v1/customer/sales-areas 替代 /erp/customer/sales-areas
|
||||
* - /erp/dynamic/v1/customer/brands 替代 /erp/customer/brands
|
||||
*
|
||||
* @author HZHub Team
|
||||
* @since 2026-04-21
|
||||
* @deprecated since 2026-04-30, will be removed in 2026-07-30
|
||||
*/
|
||||
@Deprecated(since = "2026-04-30", forRemoval = true)
|
||||
@SaIgnore
|
||||
@RestController
|
||||
@RequestMapping("/erp/customer")
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerController extends BaseController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CustomerController.class);
|
||||
private final ICustomerService customerService;
|
||||
|
||||
/**
|
||||
* 分页查询客户列表
|
||||
* @deprecated 请使用动态API: /erp/dynamic/v1/customer/list
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<CustomerVO> list(
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String companyCode,
|
||||
@RequestParam(required = false) String salesAreaCode,
|
||||
@RequestParam(required = false) String brand) {
|
||||
log.warn("⚠️ 已废弃API被调用: /erp/customer/list,请迁移到动态API: /erp/dynamic/v1/customer/list");
|
||||
return customerService.queryCustomerList(pageNum, pageSize, keyword, companyCode, salesAreaCode, brand);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户详情
|
||||
*/
|
||||
@GetMapping("/{customerCode}")
|
||||
public R<CustomerVO> detail(@PathVariable String customerCode) {
|
||||
CustomerVO vo = customerService.getCustomerDetail(customerCode);
|
||||
if (vo == null) {
|
||||
return R.fail("客户不存在");
|
||||
}
|
||||
return R.ok(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有销区列表
|
||||
*/
|
||||
@GetMapping("/sales-areas")
|
||||
public R<List<CustomerVO>> salesAreas() {
|
||||
return R.ok(customerService.getSalesAreas());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有品牌列表
|
||||
*/
|
||||
@GetMapping("/brands")
|
||||
public R<List<CustomerVO>> brands() {
|
||||
return R.ok(customerService.getBrands());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package org.hzhub.erp.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.hzhub.erp.common.domain.R;
|
||||
import org.hzhub.erp.domain.entity.ErpApiConfig;
|
||||
import org.hzhub.erp.domain.vo.ApiExecutionResult;
|
||||
import org.hzhub.erp.service.IErpApiService;
|
||||
import org.hzhub.erp.service.impl.ApiStatsRecorder;
|
||||
import org.hzhub.erp.service.impl.DynamicApiExecutor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 动态API执行Controller
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/erp/dynamic")
|
||||
@RequiredArgsConstructor
|
||||
@SaIgnore // 当前版本暂时忽略认证,后续通过配置表的require_auth字段控制
|
||||
public class DynamicApiController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DynamicApiController.class);
|
||||
|
||||
private final IErpApiService erpApiService;
|
||||
private final DynamicApiExecutor dynamicApiExecutor;
|
||||
private final ApiStatsRecorder apiStatsRecorder;
|
||||
|
||||
/**
|
||||
* 动态路由处理(GET方法)- 支持多层级路径
|
||||
*
|
||||
* @param version API版本(v1/v2)
|
||||
* @param request HTTP请求对象
|
||||
* @param allParams 所有请求参数
|
||||
* @return 执行结果
|
||||
*/
|
||||
@GetMapping("/{version}/**")
|
||||
public R<Object> handleDynamicGet(@PathVariable String version,
|
||||
HttpServletRequest request,
|
||||
@RequestParam Map<String, Object> allParams) {
|
||||
String fullPath = extractFullPath(request, version);
|
||||
return executeDynamicApi(fullPath, "GET", allParams, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态路由处理(POST方法)- 支持多层级路径
|
||||
*
|
||||
* @param version API版本(v1/v2)
|
||||
* @param request HTTP请求对象
|
||||
* @param bodyParams 请求体参数
|
||||
* @return 执行结果
|
||||
*/
|
||||
@PostMapping("/{version}/**")
|
||||
public R<Object> handleDynamicPost(@PathVariable String version,
|
||||
HttpServletRequest request,
|
||||
@RequestBody Map<String, Object> bodyParams) {
|
||||
String fullPath = extractFullPath(request, version);
|
||||
return executeDynamicApi(fullPath, "POST", bodyParams, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中提取完整的API路径
|
||||
*
|
||||
* @param request HTTP请求对象
|
||||
* @param version API版本
|
||||
* @return 完整路径(如:/erp/dynamic/v1/customer/brands)
|
||||
*/
|
||||
private String extractFullPath(HttpServletRequest request, String version) {
|
||||
String servletPath = request.getServletPath();
|
||||
log.debug("Servlet path: {}, version: {}", servletPath, version);
|
||||
return servletPath; // 直接返回完整路径,Spring已正确解析
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行动态API
|
||||
*
|
||||
* @param fullPath 完整API路径
|
||||
* @param method HTTP方法
|
||||
* @param params 参数Map
|
||||
* @param request HTTP请求对象
|
||||
* @return 执行结果
|
||||
*/
|
||||
private R<Object> executeDynamicApi(String fullPath, String method, Map<String, Object> params, HttpServletRequest request) {
|
||||
// 记录开始时间
|
||||
long startTime = System.currentTimeMillis();
|
||||
ErpApiConfig config = null;
|
||||
|
||||
try {
|
||||
// 从完整路径中提取版本号(路径格式:/erp/dynamic/v1/customer/brands)
|
||||
String version = fullPath.split("/")[3]; // 第4段是版本号
|
||||
String apiPath = fullPath.substring("/erp/dynamic/".length() + version.length() + 1);
|
||||
|
||||
log.info("执行动态API: fullPath={}, method={}, version={}, apiPath={}, params={}",
|
||||
fullPath, method, version, apiPath, params);
|
||||
|
||||
// 1. 查询API配置(使用完整路径匹配)
|
||||
config = erpApiService.selectApiConfigByPath(fullPath, method, version);
|
||||
if (config == null) {
|
||||
log.warn("API配置不存在: path={}, method={}, version={}", fullPath, method, version);
|
||||
return R.fail("API不存在");
|
||||
}
|
||||
|
||||
if (config.getStatus() == 0) {
|
||||
log.warn("API已禁用: apiId={}", config.getApiId());
|
||||
return R.fail("API已禁用");
|
||||
}
|
||||
|
||||
// 2. 权限检查(如果配置了require_auth)
|
||||
// TODO: 集成Sa-Token权限验证(Phase 4)
|
||||
if (config.getRequireAuth() == 1) {
|
||||
log.info("API需要权限验证: permissionCode={}", config.getPermissionCode());
|
||||
// StpUtil.checkPermission(config.getPermissionCode());
|
||||
}
|
||||
|
||||
// 3. 参数验证与转换
|
||||
// TODO: 根据参数配置验证参数(Phase 4)
|
||||
|
||||
// 4. 执行SQL
|
||||
ApiExecutionResult executionResult = dynamicApiExecutor.execute(config, params);
|
||||
Object result = executionResult.getData();
|
||||
String executedSql = executionResult.getExecutedSql();
|
||||
|
||||
// 计算响应时间
|
||||
long responseTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
log.info("动态API执行成功: apiId={}, resultType={}, time={}ms",
|
||||
config.getApiId(), result.getClass().getSimpleName(), responseTime);
|
||||
log.debug("实际执行SQL: {}", executedSql);
|
||||
|
||||
// 5. 记录成功统计(异步记录,不影响响应)
|
||||
try {
|
||||
String callParamsJson = params.toString();
|
||||
String clientIp = getClientIp(request);
|
||||
String userId = getUserId(request);
|
||||
apiStatsRecorder.recordSuccess(config.getApiId(), callParamsJson, executedSql,
|
||||
responseTime, clientIp, userId);
|
||||
} catch (Exception statsError) {
|
||||
log.warn("统计记录失败(不影响API响应): {}", statsError.getMessage());
|
||||
}
|
||||
|
||||
// 6. 返回结果
|
||||
if (result instanceof R) {
|
||||
return (R<Object>) result;
|
||||
} else {
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
} catch (SecurityException e) {
|
||||
log.error("安全验证失败: {}", e.getMessage());
|
||||
|
||||
// 记录错误统计
|
||||
if (config != null) {
|
||||
long responseTime = System.currentTimeMillis() - startTime;
|
||||
// 错误情况下使用SQL模板作为executedSql
|
||||
String executedSql = config.getSqlTemplate();
|
||||
apiStatsRecorder.recordError(config.getApiId(), params.toString(), executedSql,
|
||||
responseTime, e.getMessage(),
|
||||
getStackTrace(e), getClientIp(request), getUserId(request));
|
||||
}
|
||||
|
||||
return R.fail("安全验证失败: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("动态API执行失败: {}", e.getMessage(), e);
|
||||
|
||||
// 记录错误统计
|
||||
if (config != null) {
|
||||
long responseTime = System.currentTimeMillis() - startTime;
|
||||
// 错误情况下使用SQL模板作为executedSql
|
||||
String executedSql = config.getSqlTemplate();
|
||||
apiStatsRecorder.recordError(config.getApiId(), params.toString(), executedSql,
|
||||
responseTime, e.getMessage(),
|
||||
getStackTrace(e), getClientIp(request), getUserId(request));
|
||||
}
|
||||
|
||||
return R.fail("执行失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP(考虑代理和网关)
|
||||
*/
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||
}
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
}
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP"); // Nginx常用的真实IP头
|
||||
}
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr(); // 最后使用直接连接的IP
|
||||
}
|
||||
|
||||
// 如果通过了多个代理,第一个IP才是真实IP
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
|
||||
return ip != null ? ip : "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户ID(从网关注入的请求头中获取)
|
||||
*/
|
||||
private String getUserId(HttpServletRequest request) {
|
||||
// 网关会在验证JWT后注入用户ID
|
||||
String userId = request.getHeader("X-User-Id");
|
||||
|
||||
if (userId == null || userId.isEmpty()) {
|
||||
// 如果没有网关头,尝试从Sa-Token获取
|
||||
try {
|
||||
// StpUtil.getLoginIdAsString(); // 如果已集成Sa-Token
|
||||
userId = "anonymous";
|
||||
} catch (Exception e) {
|
||||
userId = "anonymous";
|
||||
}
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常堆栈(前500字符)
|
||||
*/
|
||||
private String getStackTrace(Exception e) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (StackTraceElement element : e.getStackTrace()) {
|
||||
sb.append(element.toString()).append("\n");
|
||||
if (sb.length() > 500) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package org.hzhub.erp.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.hzhub.erp.common.core.BaseController;
|
||||
import org.hzhub.erp.common.domain.R;
|
||||
import org.hzhub.erp.common.page.TableDataInfo;
|
||||
import org.hzhub.erp.domain.entity.ErpApiConfig;
|
||||
import org.hzhub.erp.domain.entity.ErpApiParam;
|
||||
import org.hzhub.erp.domain.vo.ApiTestResultVO;
|
||||
import org.hzhub.erp.domain.vo.ErpApiConfigVO;
|
||||
import org.hzhub.erp.service.IErpApiService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ERP动态API配置Controller
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@SaIgnore // 开发阶段暂时跳过认证,生产环境需要移除此注解并启用权限验证
|
||||
@RestController
|
||||
@RequestMapping("/erp/api/config")
|
||||
@RequiredArgsConstructor
|
||||
public class ErpApiController extends BaseController {
|
||||
|
||||
private final IErpApiService erpApiService;
|
||||
|
||||
/**
|
||||
* 分页查询API配置列表
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<ErpApiConfigVO> list(ErpApiConfigVO query,
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
return erpApiService.queryApiConfigList(query, pageNum, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API配置详情(含参数列表)
|
||||
*/
|
||||
@GetMapping("/{apiId}")
|
||||
public R<Map<String, Object>> getInfo(@PathVariable Long apiId) {
|
||||
ErpApiConfig config = erpApiService.selectApiConfigById(apiId);
|
||||
List<ErpApiParam> params = erpApiService.selectApiParamsByApiId(apiId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("info", config);
|
||||
result.put("params", params);
|
||||
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增API配置
|
||||
*/
|
||||
@PostMapping
|
||||
public R<Void> add(@RequestBody @Validated ErpApiConfig config) {
|
||||
int rows = erpApiService.insertApiConfig(config);
|
||||
return rows > 0 ? R.ok() : R.fail("新增失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改API配置
|
||||
*/
|
||||
@PutMapping
|
||||
public R<Void> edit(@RequestBody @Validated ErpApiConfig config) {
|
||||
int rows = erpApiService.updateApiConfig(config);
|
||||
return rows > 0 ? R.ok() : R.fail("修改失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除API配置
|
||||
*/
|
||||
@DeleteMapping("/{apiIds}")
|
||||
public R<Void> remove(@PathVariable Long[] apiIds) {
|
||||
int rows = erpApiService.deleteApiConfigByIds(apiIds);
|
||||
return rows > 0 ? R.ok() : R.fail("删除失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用API
|
||||
*/
|
||||
@PutMapping("/changeStatus")
|
||||
public R<Void> changeStatus(@RequestBody ErpApiConfig config) {
|
||||
int rows = erpApiService.updateApiStatus(config);
|
||||
return rows > 0 ? R.ok() : R.fail("状态更新失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库表导入生成初始配置
|
||||
*/
|
||||
@PostMapping("/importFromTable")
|
||||
public R<Void> importFromTable(@RequestBody Map<String, Object> request) {
|
||||
String[] tableNames = ((List<String>) request.get("tableNames")).toArray(new String[0]);
|
||||
String dataSource = (String) request.getOrDefault("dataSource", "erp");
|
||||
|
||||
erpApiService.importFromTable(tableNames, dataSource);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步表结构(更新字段配置)
|
||||
*/
|
||||
@GetMapping("/syncTable/{apiId}")
|
||||
public R<Void> syncTable(@PathVariable Long apiId) {
|
||||
erpApiService.syncTableStructure(apiId);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* API测试
|
||||
*/
|
||||
@PostMapping("/test/{apiId}")
|
||||
public R<ApiTestResultVO> testApi(@PathVariable Long apiId,
|
||||
@RequestBody Map<String, Object> testParams,
|
||||
HttpServletRequest request) {
|
||||
// 提取客户端IP和用户ID
|
||||
String clientIp = getClientIp(request);
|
||||
String userId = getUserId(request);
|
||||
|
||||
ApiTestResultVO result = erpApiService.testApi(apiId, testParams, clientIp, userId);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP(考虑代理和网关)
|
||||
*/
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||
}
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
}
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP"); // Nginx常用的真实IP头
|
||||
}
|
||||
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr(); // 最后使用直接连接的IP
|
||||
}
|
||||
|
||||
// 如果通过了多个代理,第一个IP才是真实IP
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
|
||||
return ip != null ? ip : "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户ID(从网关注入的请求头中获取)
|
||||
*/
|
||||
private String getUserId(HttpServletRequest request) {
|
||||
// 网关会在验证JWT后注入用户ID
|
||||
String userId = request.getHeader("X-User-Id");
|
||||
|
||||
if (userId == null || userId.isEmpty()) {
|
||||
// 如果没有网关头,尝试从Sa-Token获取
|
||||
try {
|
||||
// StpUtil.getLoginIdAsString(); // 如果已集成Sa-Token
|
||||
userId = "test-user"; // 测试环境使用固定标识
|
||||
} catch (Exception e) {
|
||||
userId = "test-user";
|
||||
}
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* API文档预览
|
||||
*/
|
||||
@GetMapping("/preview/{apiId}")
|
||||
public R<Map<String, String>> preview(@PathVariable Long apiId) {
|
||||
Map<String, String> docMap = erpApiService.generateApiDoc(apiId);
|
||||
return R.ok(docMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询调用统计
|
||||
*/
|
||||
@GetMapping("/stats/{apiId}")
|
||||
public R<Map<String, Object>> getStats(@PathVariable Long apiId,
|
||||
@RequestParam(required = false) String startTime,
|
||||
@RequestParam(required = false) String endTime) {
|
||||
Map<String, Object> stats = erpApiService.getApiStats(apiId, startTime, endTime);
|
||||
return R.ok(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询错误日志
|
||||
*/
|
||||
@GetMapping("/errorLog/{apiId}")
|
||||
public R<List<Map<String, Object>>> getErrorLog(@PathVariable Long apiId,
|
||||
@RequestParam(defaultValue = "10") Integer limit) {
|
||||
List<Map<String, Object>> logs = erpApiService.getApiErrorLog(apiId, limit);
|
||||
return R.ok(logs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
@DeleteMapping("/cache/{apiId}")
|
||||
public R<Void> clearCache(@PathVariable Long apiId) {
|
||||
erpApiService.clearApiCache(apiId);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.hzhub.erp.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.hzhub.erp.common.domain.R;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ERP 数据库探查工具
|
||||
*/
|
||||
@SaIgnore
|
||||
@RestController
|
||||
@RequestMapping("/erp/test")
|
||||
@RequiredArgsConstructor
|
||||
public class ErpExploreController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ErpExploreController.class);
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Value("${spring.datasource.url}")
|
||||
private String datasourceUrl;
|
||||
|
||||
@Value("${spring.datasource.username}")
|
||||
private String datasourceUsername;
|
||||
|
||||
/**
|
||||
* 探查数据库所有表,返回统计信息
|
||||
*/
|
||||
@GetMapping("/explore")
|
||||
public R<Map<String, Object>> exploreDatabase() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
// 获取数据库名
|
||||
String dbName = jdbcTemplate.queryForObject("SELECT DB_NAME()", String.class);
|
||||
result.put("database", dbName);
|
||||
|
||||
// 获取所有用户表及其基本信息
|
||||
String sql =
|
||||
"SELECT " +
|
||||
" t.name AS tableName, " +
|
||||
" SCHEMA_NAME(t.schema_id) AS schemaName, " +
|
||||
" p.rows AS rowCount, " +
|
||||
" c2.columnCount, " +
|
||||
" CASE WHEN pk.colCount > 0 THEN 1 ELSE 0 END AS hasPrimaryKey " +
|
||||
"FROM sys.tables t " +
|
||||
"CROSS APPLY ( " +
|
||||
" SELECT COUNT(*) AS columnCount " +
|
||||
" FROM sys.columns c " +
|
||||
" WHERE c.object_id = t.object_id " +
|
||||
") c2 " +
|
||||
"LEFT JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1) " +
|
||||
"OUTER APPLY ( " +
|
||||
" SELECT COUNT(*) AS colCount " +
|
||||
" FROM sys.index_columns ic " +
|
||||
" INNER JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id " +
|
||||
" WHERE ic.object_id = t.object_id AND i.is_primary_key = 1 " +
|
||||
") pk " +
|
||||
"WHERE t.type = 'U' " +
|
||||
"ORDER BY p.rows DESC";
|
||||
|
||||
List<Map<String, Object>> tables = jdbcTemplate.queryForList(sql);
|
||||
result.put("tables", tables);
|
||||
result.put("totalTables", tables.size());
|
||||
|
||||
return R.ok("数据库探查成功", result);
|
||||
} catch (Exception e) {
|
||||
log.error("数据库探查失败", e);
|
||||
return R.fail("数据库探查失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看指定表的列信息
|
||||
*/
|
||||
@GetMapping("/explore/table")
|
||||
public R<Map<String, Object>> exploreTable(@org.springframework.web.bind.annotation.RequestParam String tableName) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
String sql =
|
||||
"SELECT " +
|
||||
" c.name AS columnName, " +
|
||||
" TYPE_NAME(c.user_type_id) AS dataType, " +
|
||||
" c.max_length AS maxLength, " +
|
||||
" c.is_nullable AS nullable, " +
|
||||
" c.is_identity AS isIdentity, " +
|
||||
" ep.value AS description " +
|
||||
"FROM sys.columns c " +
|
||||
"INNER JOIN sys.tables t ON c.object_id = t.object_id " +
|
||||
"LEFT JOIN sys.extended_properties ep ON c.object_id = ep.major_id " +
|
||||
" AND c.column_id = ep.minor_id AND ep.name = 'MS_Description' " +
|
||||
"WHERE t.name = ? " +
|
||||
"ORDER BY c.column_id";
|
||||
List<Map<String, Object>> columns = jdbcTemplate.queryForList(sql, tableName);
|
||||
result.put("tableName", tableName);
|
||||
result.put("columns", columns);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
return R.fail("查询表结构失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.hzhub.erp.controller;
|
||||
|
||||
import org.hzhub.erp.common.domain.R;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* ERP测试Controller(用于验证认证问题)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/erp/test2")
|
||||
public class ErpTest2Controller {
|
||||
|
||||
@GetMapping("/hello")
|
||||
public R<String> hello() {
|
||||
return R.ok("Hello, ERP Test2");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.hzhub.erp.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.hzhub.erp.common.domain.R;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ERP 连接测试控制器
|
||||
*/
|
||||
@SaIgnore
|
||||
@RestController
|
||||
@RequestMapping("/erp/test")
|
||||
@RequiredArgsConstructor
|
||||
public class ErpTestController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ErpTestController.class);
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 测试 SQL Server 数据库连接
|
||||
*/
|
||||
@GetMapping("/connection")
|
||||
public R<Map<String, String>> testConnection() {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
try {
|
||||
String version = jdbcTemplate.queryForObject("SELECT @@VERSION", String.class);
|
||||
String dbName = jdbcTemplate.queryForObject("SELECT DB_NAME()", String.class);
|
||||
result.put("status", "connected");
|
||||
result.put("database", dbName != null ? dbName : "unknown");
|
||||
result.put("version", version != null ? version.substring(0, Math.min(150, version.length())) : "unknown");
|
||||
return R.ok("SQL Server 连接成功", result);
|
||||
} catch (Exception e) {
|
||||
log.error("SQL Server 连接失败", e);
|
||||
Throwable cause = e;
|
||||
while (cause.getCause() != null) {
|
||||
cause = cause.getCause();
|
||||
}
|
||||
result.put("status", "failed");
|
||||
result.put("error", e.getMessage());
|
||||
result.put("rootCause", cause.getClass().getName() + ": " + cause.getMessage());
|
||||
return R.fail("SQL Server 连接失败: " + e.getMessage(), result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public R<String> health() {
|
||||
return R.ok("hzhub-erp is running");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.hzhub.erp.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 客户档案实体(SCLTGENERAL 表)
|
||||
*/
|
||||
@Data
|
||||
@TableName("SCLTGENERAL")
|
||||
public class CustomerGeneral {
|
||||
|
||||
@TableId("CLTCODE")
|
||||
private String cltCode;
|
||||
|
||||
@TableField("CLTNAME")
|
||||
private String cltName;
|
||||
|
||||
@TableField("COMPANY_ID")
|
||||
private String companyId;
|
||||
|
||||
@TableField("COMPANY_NAME")
|
||||
private String companyName;
|
||||
|
||||
@TableField("BRAND")
|
||||
private String brand;
|
||||
|
||||
@TableField("BRANDNAME")
|
||||
private String brandName;
|
||||
|
||||
@TableField("LINKMAN")
|
||||
private String linkMan;
|
||||
|
||||
@TableField("AREAID")
|
||||
private String areaId;
|
||||
|
||||
@TableField("AREANAME")
|
||||
private String areaName;
|
||||
|
||||
@TableField("SALESID_T")
|
||||
private String salesId;
|
||||
|
||||
@TableField("SALESNAME_T")
|
||||
private String salesName;
|
||||
|
||||
@TableField("SALEDOCID")
|
||||
private String saleDocId;
|
||||
|
||||
@TableField("SALEDOCNAME")
|
||||
private String saleDocName;
|
||||
|
||||
@TableField("CLTPRICENO")
|
||||
private String cltPriceNo;
|
||||
|
||||
@TableField("CLTPRICENAME")
|
||||
private String cltPriceName;
|
||||
|
||||
@TableField("CLTTYPE")
|
||||
private String cltType;
|
||||
|
||||
@TableField("STREET")
|
||||
private String street;
|
||||
|
||||
@TableField("TEL1")
|
||||
private String tel1;
|
||||
|
||||
@TableField("TEL2")
|
||||
private String tel2;
|
||||
|
||||
@TableField("EMAIL")
|
||||
private String email;
|
||||
|
||||
@TableField("SDORGID")
|
||||
private String sdOrgId;
|
||||
|
||||
@TableField("SDORGNAME")
|
||||
private String sdOrgName;
|
||||
|
||||
@TableField("ISSTOP")
|
||||
private Integer isStop;
|
||||
|
||||
@TableField("CREATE_DATE")
|
||||
private LocalDateTime createDate;
|
||||
|
||||
@TableField("CREATE_NAME")
|
||||
private String createName;
|
||||
|
||||
@TableField("administrative")
|
||||
private String administrative;
|
||||
|
||||
@TableField("administraname")
|
||||
private String administraname;
|
||||
|
||||
@TableField("province")
|
||||
private String province;
|
||||
|
||||
@TableField("city")
|
||||
private String city;
|
||||
|
||||
@TableField("Country")
|
||||
private String country;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.hzhub.erp.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* ERP动态API配置实体
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Data
|
||||
@TableName("erp_api_config")
|
||||
public class ErpApiConfig implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** API ID */
|
||||
@TableId(value = "api_id", type = IdType.AUTO)
|
||||
private Long apiId;
|
||||
|
||||
/** API名称 */
|
||||
private String apiName;
|
||||
|
||||
/** API路径(如 /erp/dynamic/customer/list) */
|
||||
private String apiPath;
|
||||
|
||||
/** HTTP方法(GET/POST) */
|
||||
private String apiMethod;
|
||||
|
||||
/** API描述 */
|
||||
private String apiDesc;
|
||||
|
||||
/** API版本号(v1/v2) */
|
||||
private String apiVersion;
|
||||
|
||||
/** 数据源名称 */
|
||||
private String dataSource;
|
||||
|
||||
/** SQL模板(支持参数占位符 #{paramName}) */
|
||||
private String sqlTemplate;
|
||||
|
||||
/** 结果类型(LIST/SINGLE/COUNT) */
|
||||
private String resultType;
|
||||
|
||||
/** 是否支持分页 */
|
||||
private Integer supportPagination;
|
||||
|
||||
/** 页码参数名 */
|
||||
private String pageParamName;
|
||||
|
||||
/** 页大小参数名 */
|
||||
private String sizeParamName;
|
||||
|
||||
/** 是否需要认证 */
|
||||
private Integer requireAuth;
|
||||
|
||||
/** 权限标识(如 erp:customer:list) */
|
||||
private String permissionCode;
|
||||
|
||||
/** 是否启用缓存 */
|
||||
private Integer enableCache;
|
||||
|
||||
/** 缓存键模板(支持参数占位符) */
|
||||
private String cacheKeyTemplate;
|
||||
|
||||
/** 缓存过期时间(秒) */
|
||||
private Integer cacheTtl;
|
||||
|
||||
/** 来源表名 */
|
||||
private String sourceTable;
|
||||
|
||||
/** 来源表描述 */
|
||||
private String sourceTableComment;
|
||||
|
||||
/** 状态(0禁用 1启用) */
|
||||
private Integer status;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/** 创建者 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/** 更新者 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.hzhub.erp.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* ERP动态API参数配置实体
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Data
|
||||
@TableName("erp_api_param")
|
||||
public class ErpApiParam implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 参数ID */
|
||||
@TableId(value = "param_id", type = IdType.AUTO)
|
||||
private Long paramId;
|
||||
|
||||
/** 所属API ID */
|
||||
private Long apiId;
|
||||
|
||||
/** 参数名称 */
|
||||
private String paramName;
|
||||
|
||||
/** 参数描述 */
|
||||
private String paramDesc;
|
||||
|
||||
/** 参数类型(String/Integer/Long/Date/Boolean) */
|
||||
private String paramType;
|
||||
|
||||
/** 参数位置(QUERY/BODY) */
|
||||
private String paramPosition;
|
||||
|
||||
/** 是否必填 */
|
||||
private Integer isRequired;
|
||||
|
||||
/** 默认值 */
|
||||
private String defaultValue;
|
||||
|
||||
/** SQL参数名 */
|
||||
private String sqlParamName;
|
||||
|
||||
/** 排序 */
|
||||
private Integer sort;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.hzhub.erp.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* ERP动态API调用统计实体
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Data
|
||||
@TableName("erp_api_stats")
|
||||
public class ErpApiStats implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 统计ID */
|
||||
@TableId(value = "stats_id", type = IdType.AUTO)
|
||||
private Long statsId;
|
||||
|
||||
/** API ID */
|
||||
private Long apiId;
|
||||
|
||||
/** 调用时间 */
|
||||
private LocalDateTime callTime;
|
||||
|
||||
/** 调用参数(JSON) */
|
||||
private String callParams;
|
||||
|
||||
/** 响应时间(ms) */
|
||||
private Integer responseTime;
|
||||
|
||||
/** 调用状态(SUCCESS/ERROR) */
|
||||
private String callStatus;
|
||||
|
||||
/** 错误消息 */
|
||||
private String errorMessage;
|
||||
|
||||
/** 错误堆栈 */
|
||||
private String errorStack;
|
||||
|
||||
/** 客户端IP */
|
||||
private String clientIp;
|
||||
|
||||
/** 用户ID */
|
||||
private String userId;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.hzhub.erp.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 销售组织实体(OSDORG 表)
|
||||
*/
|
||||
@Data
|
||||
@TableName("OSDORG")
|
||||
public class SalesOrganization {
|
||||
|
||||
/**
|
||||
* 销售组织编码
|
||||
*/
|
||||
@TableId
|
||||
private String orgCode;
|
||||
|
||||
/**
|
||||
* 销售组织名称
|
||||
*/
|
||||
private String orgName;
|
||||
|
||||
/**
|
||||
* 父组织编码
|
||||
*/
|
||||
private String parentOrgCode;
|
||||
|
||||
/**
|
||||
* 组织层级
|
||||
*/
|
||||
private Integer orgLevel;
|
||||
|
||||
/**
|
||||
* 是否启用(1-启用,0-停用)
|
||||
*/
|
||||
private Integer isEnable;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.hzhub.erp.domain.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* API执行结果包装类
|
||||
* 包含执行结果和实际执行的SQL
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiExecutionResult {
|
||||
/**
|
||||
* 执行结果数据
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 实际执行的SQL语句(已替换参数)
|
||||
*/
|
||||
private String executedSql;
|
||||
|
||||
/**
|
||||
* 创建成功结果
|
||||
*/
|
||||
public static ApiExecutionResult success(Object data, String executedSql) {
|
||||
return new ApiExecutionResult(data, executedSql);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.hzhub.erp.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* API测试结果VO
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Data
|
||||
public class ApiTestResultVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** API路径 */
|
||||
private String apiPath;
|
||||
|
||||
/** 测试方法 */
|
||||
private String testMethod;
|
||||
|
||||
/** 请求参数 */
|
||||
private Map<String, Object> requestParams;
|
||||
|
||||
/** 执行成功 */
|
||||
private Boolean success;
|
||||
|
||||
/** 执行结果 */
|
||||
private Object data;
|
||||
|
||||
/** 执行时间(ms) */
|
||||
private Long executionTime;
|
||||
|
||||
/** 实际执行的SQL */
|
||||
private String executedSql;
|
||||
|
||||
/** 错误消息 */
|
||||
private String errorMessage;
|
||||
|
||||
/** 错误堆栈 */
|
||||
private String errorStack;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.hzhub.erp.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* 客户档案视图对象(SCLTGENERAL 统一查询结果)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CustomerVO {
|
||||
|
||||
/** 客户编号 */
|
||||
private String customerCode;
|
||||
|
||||
/** 客户名称 */
|
||||
private String customerName;
|
||||
|
||||
/** 公司编号 */
|
||||
private String companyCode;
|
||||
|
||||
/** 公司名称 */
|
||||
private String companyName;
|
||||
|
||||
/** 品牌 */
|
||||
private String brand;
|
||||
|
||||
/** 品牌名称 */
|
||||
private String brandName;
|
||||
|
||||
/** 联系人 */
|
||||
private String contactName;
|
||||
|
||||
/** 销区编号 */
|
||||
private String salesAreaCode;
|
||||
|
||||
/** 销区名称 */
|
||||
private String salesAreaName;
|
||||
|
||||
/** 业务员编号 */
|
||||
private String salesPersonCode;
|
||||
|
||||
/** 业务员姓名 */
|
||||
private String salesPersonName;
|
||||
|
||||
/** 销售负责人编号 */
|
||||
private String saleDocCode;
|
||||
|
||||
/** 销售负责人姓名 */
|
||||
private String saleDocName;
|
||||
|
||||
/** 价格方案号 */
|
||||
private String pricePlanCode;
|
||||
|
||||
/** 价格方案名称 */
|
||||
private String pricePlanName;
|
||||
|
||||
/** 客户类型 */
|
||||
private String customerType;
|
||||
|
||||
/** 地址 */
|
||||
private String address;
|
||||
|
||||
/** 电话 */
|
||||
private String phone;
|
||||
|
||||
/** 邮箱 */
|
||||
private String email;
|
||||
|
||||
/** 经销组织编号 */
|
||||
private String sdOrgCode;
|
||||
|
||||
/** 经销组织名称 */
|
||||
private String sdOrgName;
|
||||
|
||||
/** 省份 */
|
||||
private String province;
|
||||
|
||||
/** 城市 */
|
||||
private String city;
|
||||
|
||||
/** 是否停用 */
|
||||
private Integer isStop;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.hzhub.erp.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.time.LocalDateTime;
|
||||
import org.hzhub.erp.domain.entity.ErpApiParam;
|
||||
|
||||
/**
|
||||
* ERP动态API配置VO(包含参数列表)
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Data
|
||||
public class ErpApiConfigVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** API ID */
|
||||
private Long apiId;
|
||||
|
||||
/** API名称 */
|
||||
private String apiName;
|
||||
|
||||
/** API路径 */
|
||||
private String apiPath;
|
||||
|
||||
/** HTTP方法 */
|
||||
private String apiMethod;
|
||||
|
||||
/** API描述 */
|
||||
private String apiDesc;
|
||||
|
||||
/** API版本号 */
|
||||
private String apiVersion;
|
||||
|
||||
/** 数据源名称 */
|
||||
private String dataSource;
|
||||
|
||||
/** SQL模板 */
|
||||
private String sqlTemplate;
|
||||
|
||||
/** 结果类型 */
|
||||
private String resultType;
|
||||
|
||||
/** 是否支持分页 */
|
||||
private Integer supportPagination;
|
||||
|
||||
/** 页码参数名 */
|
||||
private String pageParamName;
|
||||
|
||||
/** 页大小参数名 */
|
||||
private String sizeParamName;
|
||||
|
||||
/** 是否需要认证 */
|
||||
private Integer requireAuth;
|
||||
|
||||
/** 权限标识 */
|
||||
private String permissionCode;
|
||||
|
||||
/** 是否启用缓存 */
|
||||
private Integer enableCache;
|
||||
|
||||
/** 缓存键模板 */
|
||||
private String cacheKeyTemplate;
|
||||
|
||||
/** 缓存过期时间 */
|
||||
private Integer cacheTtl;
|
||||
|
||||
/** 来源表名 */
|
||||
private String sourceTable;
|
||||
|
||||
/** 来源表描述 */
|
||||
private String sourceTableComment;
|
||||
|
||||
/** 状态 */
|
||||
private Integer status;
|
||||
|
||||
/** 创建时间 */
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/** 创建者 */
|
||||
private String createBy;
|
||||
|
||||
/** 更新者 */
|
||||
private String updateBy;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 参数列表 */
|
||||
private List<ErpApiParam> params;
|
||||
}
|
||||
146
hzhub-erp/src/main/java/org/hzhub/erp/mapper/CustomerMapper.java
Normal file
146
hzhub-erp/src/main/java/org/hzhub/erp/mapper/CustomerMapper.java
Normal file
@@ -0,0 +1,146 @@
|
||||
package org.hzhub.erp.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import org.hzhub.erp.domain.entity.CustomerGeneral;
|
||||
import org.hzhub.erp.domain.vo.CustomerVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 客户档案 Mapper(SCLTGENERAL 表)
|
||||
*/
|
||||
@Mapper
|
||||
public interface CustomerMapper extends BaseMapper<CustomerGeneral> {
|
||||
|
||||
/**
|
||||
* 分页查询客户列表
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT TOP ${pageSize} * FROM (" +
|
||||
" SELECT ROW_NUMBER() OVER (ORDER BY CLTCODE) AS rn, " +
|
||||
" CLTCODE AS customerCode, " +
|
||||
" CLTNAME AS customerName, " +
|
||||
" COMPANY_ID AS companyCode, " +
|
||||
" COMPANY_NAME AS companyName, " +
|
||||
" BRAND AS brand, " +
|
||||
" BRANDNAME AS brandName, " +
|
||||
" LINKMAN AS contactName, " +
|
||||
" AREAID AS salesAreaCode, " +
|
||||
" AREANAME AS salesAreaName, " +
|
||||
" SALESID_T AS salesPersonCode, " +
|
||||
" SALESNAME_T AS salesPersonName, " +
|
||||
" SALEDOCID AS saleDocCode, " +
|
||||
" SALEDOCNAME AS saleDocName, " +
|
||||
" CLTPRICENO AS pricePlanCode, " +
|
||||
" CLTPRICENAME AS pricePlanName, " +
|
||||
" CLTTYPE AS customerType, " +
|
||||
" STREET AS address, " +
|
||||
" TEL1 AS phone, " +
|
||||
" EMAIL AS email, " +
|
||||
" SDORGID AS sdOrgCode, " +
|
||||
" SDORGNAME AS sdOrgName, " +
|
||||
" province, city, ISSTOP AS isStop " +
|
||||
" FROM SCLTGENERAL " +
|
||||
" WHERE 1=1 " +
|
||||
" <if test='keyword != null and keyword != \"\"'>" +
|
||||
" AND (CLTCODE LIKE '%' + #{keyword} + '%' " +
|
||||
" OR CLTNAME LIKE '%' + #{keyword} + '%' " +
|
||||
" OR LINKMAN LIKE '%' + #{keyword} + '%' " +
|
||||
" OR AREANAME LIKE '%' + #{keyword} + '%' " +
|
||||
" OR SALESNAME_T LIKE '%' + #{keyword} + '%') " +
|
||||
" </if>" +
|
||||
" <if test='companyCode != null and companyCode != \"\"'>" +
|
||||
" AND COMPANY_ID = #{companyCode} " +
|
||||
" </if>" +
|
||||
" <if test='salesAreaCode != null and salesAreaCode != \"\"'>" +
|
||||
" AND AREAID = #{salesAreaCode} " +
|
||||
" </if>" +
|
||||
" <if test='brand != null and brand != \"\"'>" +
|
||||
" AND BRAND = #{brand} " +
|
||||
" </if>" +
|
||||
") t WHERE rn > (${pageNum} - 1) * ${pageSize} ORDER BY rn" +
|
||||
"</script>")
|
||||
List<CustomerVO> selectCustomerPage(@Param("pageNum") int pageNum,
|
||||
@Param("pageSize") int pageSize,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("companyCode") String companyCode,
|
||||
@Param("salesAreaCode") String salesAreaCode,
|
||||
@Param("brand") String brand);
|
||||
|
||||
/**
|
||||
* 查询客户总数
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT COUNT(*) FROM SCLTGENERAL " +
|
||||
"WHERE 1=1 " +
|
||||
"<if test='keyword != null and keyword != \"\"'>" +
|
||||
" AND (CLTCODE LIKE '%' + #{keyword} + '%' " +
|
||||
" OR CLTNAME LIKE '%' + #{keyword} + '%' " +
|
||||
" OR LINKMAN LIKE '%' + #{keyword} + '%' " +
|
||||
" OR AREANAME LIKE '%' + #{keyword} + '%' " +
|
||||
" OR SALESNAME_T LIKE '%' + #{keyword} + '%') " +
|
||||
"</if>" +
|
||||
"<if test='companyCode != null and companyCode != \"\"'>" +
|
||||
" AND COMPANY_ID = #{companyCode} " +
|
||||
"</if>" +
|
||||
"<if test='salesAreaCode != null and salesAreaCode != \"\"'>" +
|
||||
" AND AREAID = #{salesAreaCode} " +
|
||||
"</if>" +
|
||||
"<if test='brand != null and brand != \"\"'>" +
|
||||
" AND BRAND = #{brand} " +
|
||||
"</if>" +
|
||||
"</script>")
|
||||
long selectCustomerCount(@Param("keyword") String keyword,
|
||||
@Param("companyCode") String companyCode,
|
||||
@Param("salesAreaCode") String salesAreaCode,
|
||||
@Param("brand") String brand);
|
||||
|
||||
/**
|
||||
* 根据客户编号获取详情
|
||||
*/
|
||||
@Select("SELECT " +
|
||||
" CLTCODE AS customerCode, " +
|
||||
" CLTNAME AS customerName, " +
|
||||
" COMPANY_ID AS companyCode, " +
|
||||
" COMPANY_NAME AS companyName, " +
|
||||
" BRAND AS brand, " +
|
||||
" BRANDNAME AS brandName, " +
|
||||
" LINKMAN AS contactName, " +
|
||||
" AREAID AS salesAreaCode, " +
|
||||
" AREANAME AS salesAreaName, " +
|
||||
" SALESID_T AS salesPersonCode, " +
|
||||
" SALESNAME_T AS salesPersonName, " +
|
||||
" SALEDOCID AS saleDocCode, " +
|
||||
" SALEDOCNAME AS saleDocName, " +
|
||||
" CLTPRICENO AS pricePlanCode, " +
|
||||
" CLTPRICENAME AS pricePlanName, " +
|
||||
" CLTTYPE AS customerType, " +
|
||||
" STREET AS address, " +
|
||||
" TEL1 AS phone, " +
|
||||
" EMAIL AS email, " +
|
||||
" SDORGID AS sdOrgCode, " +
|
||||
" SDORGNAME AS sdOrgName, " +
|
||||
" province, city, ISSTOP AS isStop " +
|
||||
"FROM SCLTGENERAL " +
|
||||
"WHERE CLTCODE = #{customerCode}")
|
||||
CustomerVO selectCustomerDetail(@Param("customerCode") String customerCode);
|
||||
|
||||
/**
|
||||
* 获取所有销区列表
|
||||
*/
|
||||
@Select("SELECT DISTINCT AREAID AS salesAreaCode, AREANAME AS salesAreaName " +
|
||||
"FROM SCLTGENERAL " +
|
||||
"WHERE AREAID IS NOT NULL AND AREANAME IS NOT NULL " +
|
||||
"ORDER BY AREAID")
|
||||
List<CustomerVO> selectSalesAreas();
|
||||
|
||||
/**
|
||||
* 获取所有品牌列表
|
||||
*/
|
||||
@Select("SELECT DISTINCT BRAND AS brand, BRANDNAME AS brandName " +
|
||||
"FROM SCLTGENERAL " +
|
||||
"WHERE BRAND IS NOT NULL AND BRANDNAME IS NOT NULL " +
|
||||
"ORDER BY BRAND")
|
||||
List<CustomerVO> selectBrands();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.hzhub.erp.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.hzhub.erp.domain.entity.ErpApiConfig;
|
||||
|
||||
/**
|
||||
* ERP动态API配置Mapper
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Mapper
|
||||
public interface ErpApiConfigMapper extends BaseMapper<ErpApiConfig> {
|
||||
|
||||
/**
|
||||
* 根据API路径和方法查询配置
|
||||
*
|
||||
* @param apiPath API路径
|
||||
* @param apiMethod HTTP方法
|
||||
* @param apiVersion API版本
|
||||
* @return API配置
|
||||
*/
|
||||
ErpApiConfig selectByPathAndMethod(@Param("apiPath") String apiPath,
|
||||
@Param("apiMethod") String apiMethod,
|
||||
@Param("apiVersion") String apiVersion);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.hzhub.erp.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.hzhub.erp.domain.entity.ErpApiParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ERP动态API参数配置Mapper
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Mapper
|
||||
public interface ErpApiParamMapper extends BaseMapper<ErpApiParam> {
|
||||
|
||||
/**
|
||||
* 根据API ID查询参数列表
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @return 参数列表
|
||||
*/
|
||||
List<ErpApiParam> selectByApiId(@Param("apiId") Long apiId);
|
||||
|
||||
/**
|
||||
* 批量插入参数
|
||||
*
|
||||
* @param params 参数列表
|
||||
* @return 插入数量
|
||||
*/
|
||||
int batchInsert(@Param("params") List<ErpApiParam> params);
|
||||
|
||||
/**
|
||||
* 根据API ID删除参数
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @return 删除数量
|
||||
*/
|
||||
int deleteByApiId(@Param("apiId") Long apiId);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.hzhub.erp.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.hzhub.erp.domain.entity.ErpApiStats;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ERP动态API调用统计Mapper
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Mapper
|
||||
public interface ErpApiStatsMapper extends BaseMapper<ErpApiStats> {
|
||||
|
||||
/**
|
||||
* 根据API ID和时间范围查询统计
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 统计列表
|
||||
*/
|
||||
List<ErpApiStats> selectByApiIdAndTime(@Param("apiId") Long apiId,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 查询API的错误日志
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @param limit 限制数量
|
||||
* @return 错误日志列表
|
||||
*/
|
||||
List<ErpApiStats> selectErrorLogByApiId(@Param("apiId") Long apiId,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 统计API调用次数
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 调用次数
|
||||
*/
|
||||
Long countByApiId(@Param("apiId") Long apiId,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计API平均响应时间
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 平均响应时间(ms)
|
||||
*/
|
||||
Integer avgResponseTimeByApiId(@Param("apiId") Long apiId,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计API错误率
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 错误次数
|
||||
*/
|
||||
Long countErrorByApiId(@Param("apiId") Long apiId,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.hzhub.erp.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.hzhub.erp.domain.entity.SalesOrganization;
|
||||
import org.hzhub.erp.domain.vo.CustomerVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 销售组织 Mapper(OSDORG 表)
|
||||
*/
|
||||
@Mapper
|
||||
public interface SalesOrganizationMapper extends BaseMapper<SalesOrganization> {
|
||||
|
||||
/**
|
||||
* 获取所有销区列表(从 OSDORG 表)
|
||||
* 销区通常是销售组织的第2层或第3层
|
||||
*/
|
||||
@Select("SELECT DISTINCT ORGCODE AS salesAreaCode, ORGNAME AS salesAreaName " +
|
||||
"FROM OSDORG " +
|
||||
"WHERE ORGLEVEL = 3 " + // 假设销区是第3层,可根据实际情况调整
|
||||
" AND ORGCODE IS NOT NULL " +
|
||||
" AND ORGNAME IS NOT NULL " +
|
||||
" AND ISENABLE = 1 " +
|
||||
"ORDER BY ORGCODE")
|
||||
List<CustomerVO> selectSalesAreasFromOrg();
|
||||
|
||||
/**
|
||||
* 获取所有销售组织层级
|
||||
*/
|
||||
@Select("SELECT DISTINCT ORGLEVEL FROM OSDORG ORDER BY ORGLEVEL")
|
||||
List<Integer> selectOrgLevels();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.hzhub.erp.service;
|
||||
|
||||
import org.hzhub.erp.common.page.TableDataInfo;
|
||||
import org.hzhub.erp.domain.vo.CustomerVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 客户档案 Service 接口
|
||||
*/
|
||||
public interface ICustomerService {
|
||||
|
||||
/**
|
||||
* 分页查询客户列表
|
||||
*/
|
||||
TableDataInfo<CustomerVO> queryCustomerList(int pageNum, int pageSize, String keyword,
|
||||
String companyCode, String salesAreaCode, String brand);
|
||||
|
||||
/**
|
||||
* 获取客户详情
|
||||
*/
|
||||
CustomerVO getCustomerDetail(String customerCode);
|
||||
|
||||
/**
|
||||
* 获取所有销区列表
|
||||
*/
|
||||
List<CustomerVO> getSalesAreas();
|
||||
|
||||
/**
|
||||
* 获取所有品牌列表
|
||||
*/
|
||||
List<CustomerVO> getBrands();
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package org.hzhub.erp.service;
|
||||
|
||||
import org.hzhub.erp.common.page.TableDataInfo;
|
||||
import org.hzhub.erp.domain.entity.ErpApiConfig;
|
||||
import org.hzhub.erp.domain.entity.ErpApiParam;
|
||||
import org.hzhub.erp.domain.vo.ApiTestResultVO;
|
||||
import org.hzhub.erp.domain.vo.ErpApiConfigVO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ERP动态API配置Service接口
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
public interface IErpApiService {
|
||||
|
||||
/**
|
||||
* 分页查询API配置列表
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 页大小
|
||||
* @return 分页结果
|
||||
*/
|
||||
TableDataInfo<ErpApiConfigVO> queryApiConfigList(ErpApiConfigVO query, Integer pageNum, Integer pageSize);
|
||||
|
||||
/**
|
||||
* 根据ID查询API配置
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @return API配置
|
||||
*/
|
||||
ErpApiConfig selectApiConfigById(Long apiId);
|
||||
|
||||
/**
|
||||
* 根据API ID查询参数列表
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @return 参数列表
|
||||
*/
|
||||
List<ErpApiParam> selectApiParamsByApiId(Long apiId);
|
||||
|
||||
/**
|
||||
* 根据路径、方法、版本查询API配置
|
||||
*
|
||||
* @param apiPath API路径
|
||||
* @param apiMethod HTTP方法
|
||||
* @param apiVersion API版本
|
||||
* @return API配置
|
||||
*/
|
||||
ErpApiConfig selectApiConfigByPath(String apiPath, String apiMethod, String apiVersion);
|
||||
|
||||
/**
|
||||
* 新增API配置
|
||||
*
|
||||
* @param config API配置
|
||||
* @return 影响行数
|
||||
*/
|
||||
int insertApiConfig(ErpApiConfig config);
|
||||
|
||||
/**
|
||||
* 修改API配置
|
||||
*
|
||||
* @param config API配置
|
||||
* @return 影响行数
|
||||
*/
|
||||
int updateApiConfig(ErpApiConfig config);
|
||||
|
||||
/**
|
||||
* 批量删除API配置
|
||||
*
|
||||
* @param apiIds API ID数组
|
||||
* @return 影响行数
|
||||
*/
|
||||
int deleteApiConfigByIds(Long[] apiIds);
|
||||
|
||||
/**
|
||||
* 更新API状态
|
||||
*
|
||||
* @param config API配置(包含apiId和status)
|
||||
* @return 影响行数
|
||||
*/
|
||||
int updateApiStatus(ErpApiConfig config);
|
||||
|
||||
/**
|
||||
* 从数据库表导入生成API配置
|
||||
*
|
||||
* @param tableNames 表名数组
|
||||
* @param dataSource 数据源名称
|
||||
*/
|
||||
void importFromTable(String[] tableNames, String dataSource);
|
||||
|
||||
/**
|
||||
* 同步表结构
|
||||
*
|
||||
* @param apiId API ID
|
||||
*/
|
||||
void syncTableStructure(Long apiId);
|
||||
|
||||
/**
|
||||
* 测试API
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @param testParams 测试参数
|
||||
* @param clientIp 客户端IP
|
||||
* @param userId 用户ID
|
||||
* @return 测试结果
|
||||
*/
|
||||
ApiTestResultVO testApi(Long apiId, Map<String, Object> testParams, String clientIp, String userId);
|
||||
|
||||
/**
|
||||
* 生成API文档
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @return 文档内容Map
|
||||
*/
|
||||
Map<String, String> generateApiDoc(Long apiId);
|
||||
|
||||
/**
|
||||
* 查询API调用统计
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 统计信息
|
||||
*/
|
||||
Map<String, Object> getApiStats(Long apiId, String startTime, String endTime);
|
||||
|
||||
/**
|
||||
* 查询API错误日志
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @param limit 限制数量
|
||||
* @return 错误日志列表
|
||||
*/
|
||||
List<Map<String, Object>> getApiErrorLog(Long apiId, Integer limit);
|
||||
|
||||
/**
|
||||
* 清除API缓存
|
||||
*
|
||||
* @param apiId API ID
|
||||
*/
|
||||
void clearApiCache(Long apiId);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.hzhub.erp.service.impl;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* API统计记录服务
|
||||
* 使用MySQL数据源记录API调用统计信息
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Service
|
||||
@DS("master") // 使用MySQL数据源记录统计信息
|
||||
@RequiredArgsConstructor
|
||||
public class ApiStatsRecorder {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiStatsRecorder.class);
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 记录API调用统计
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @param callParams 调用参数(JSON字符串)
|
||||
* @param executedSql 实际执行的SQL语句
|
||||
* @param responseTime 响应时间(毫秒)
|
||||
* @param callStatus 调用状态(SUCCESS/ERROR)
|
||||
* @param errorMessage 错误消息(可选)
|
||||
* @param errorStack 错误堆栈(可选)
|
||||
* @param clientIp 客户端IP
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public void recordApiCall(Long apiId, String callParams, String executedSql,
|
||||
Long responseTime, String callStatus,
|
||||
String errorMessage, String errorStack,
|
||||
String clientIp, String userId) {
|
||||
try {
|
||||
String sql = "INSERT INTO erp_api_stats " +
|
||||
"(api_id, call_time, call_params, executed_sql, response_time, call_status, " +
|
||||
"error_message, error_stack, client_ip, user_id, create_time) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String callTime = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
|
||||
jdbcTemplate.update(sql,
|
||||
apiId,
|
||||
callTime,
|
||||
callParams,
|
||||
executedSql,
|
||||
responseTime,
|
||||
callStatus,
|
||||
errorMessage,
|
||||
errorStack,
|
||||
clientIp,
|
||||
userId,
|
||||
callTime
|
||||
);
|
||||
|
||||
log.info("API调用统计记录成功: apiId={}, status={}, time={}ms",
|
||||
apiId, callStatus, responseTime);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 统计记录失败不影响API执行,只记录日志
|
||||
log.error("记录API调用统计失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录成功的API调用
|
||||
*/
|
||||
public void recordSuccess(Long apiId, String callParams, String executedSql,
|
||||
Long responseTime, String clientIp, String userId) {
|
||||
recordApiCall(apiId, callParams, executedSql, responseTime, "SUCCESS",
|
||||
null, null, clientIp, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录失败的API调用
|
||||
*/
|
||||
public void recordError(Long apiId, String callParams, String executedSql,
|
||||
Long responseTime, String errorMessage, String errorStack,
|
||||
String clientIp, String userId) {
|
||||
recordApiCall(apiId, callParams, executedSql, responseTime, "ERROR",
|
||||
errorMessage, errorStack, clientIp, userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.hzhub.erp.service.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.hzhub.erp.common.page.TableDataInfo;
|
||||
import org.hzhub.erp.domain.vo.CustomerVO;
|
||||
import org.hzhub.erp.mapper.CustomerMapper;
|
||||
import org.hzhub.erp.mapper.SalesOrganizationMapper;
|
||||
import org.hzhub.erp.service.ICustomerService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 客户档案 Service 实现
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerServiceImpl implements ICustomerService {
|
||||
|
||||
private final CustomerMapper customerMapper;
|
||||
private final SalesOrganizationMapper salesOrganizationMapper;
|
||||
|
||||
@Override
|
||||
public TableDataInfo<CustomerVO> queryCustomerList(int pageNum, int pageSize, String keyword,
|
||||
String companyCode, String salesAreaCode, String brand) {
|
||||
long total = customerMapper.selectCustomerCount(keyword, companyCode, salesAreaCode, brand);
|
||||
List<CustomerVO> list = customerMapper.selectCustomerPage(pageNum, pageSize, keyword, companyCode, salesAreaCode, brand);
|
||||
return new TableDataInfo<>(list, total);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomerVO getCustomerDetail(String customerCode) {
|
||||
return customerMapper.selectCustomerDetail(customerCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CustomerVO> getSalesAreas() {
|
||||
// 从 OSDORG 表获取销区数据
|
||||
return salesOrganizationMapper.selectSalesAreasFromOrg();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CustomerVO> getBrands() {
|
||||
return customerMapper.selectBrands();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package org.hzhub.erp.service.impl;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.hzhub.erp.common.domain.R;
|
||||
import org.hzhub.erp.common.page.TableDataInfo;
|
||||
import org.hzhub.erp.domain.entity.ErpApiConfig;
|
||||
import org.hzhub.erp.domain.vo.ApiExecutionResult;
|
||||
import org.hzhub.erp.util.SqlValidator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 动态API执行引擎
|
||||
* 使用SQL Server数据源执行动态SQL查询(只读)
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Service
|
||||
@DS("erp") // 使用SQL Server数据源(erp)执行动态SQL
|
||||
@RequiredArgsConstructor
|
||||
public class DynamicApiExecutor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DynamicApiExecutor.class);
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 执行动态API
|
||||
*
|
||||
* @param config API配置
|
||||
* @param params 参数Map
|
||||
* @return 执行结果(包含数据和实际执行的SQL)
|
||||
*/
|
||||
public ApiExecutionResult execute(ErpApiConfig config, Map<String, Object> params) {
|
||||
String sqlTemplate = config.getSqlTemplate();
|
||||
|
||||
// 1. SQL安全验证
|
||||
if (!SqlValidator.validate(sqlTemplate)) {
|
||||
throw new SecurityException("SQL验证失败,可能存在安全风险");
|
||||
}
|
||||
|
||||
// 2. 处理SQL模板(参数占位符转换)
|
||||
ProcessedSql processedSql = processSqlTemplate(sqlTemplate, params);
|
||||
String processedSqlStr = processedSql.getSql();
|
||||
List<Object> paramValues = processedSql.getParamValues();
|
||||
|
||||
log.info("执行SQL: {}", processedSqlStr);
|
||||
log.info("参数值: {}", paramValues);
|
||||
|
||||
// 3. 根据结果类型执行
|
||||
try {
|
||||
Object result;
|
||||
String finalExecutedSql;
|
||||
|
||||
switch (config.getResultType()) {
|
||||
case "LIST":
|
||||
if (config.getSupportPagination() == 1) {
|
||||
result = executePaginatedQuery(config, processedSqlStr, paramValues);
|
||||
// 分页SQL需要重新构建完整SQL
|
||||
finalExecutedSql = buildFinalSqlWithParams(processedSqlStr, paramValues);
|
||||
} else {
|
||||
result = executeListQuery(processedSqlStr, paramValues);
|
||||
finalExecutedSql = buildFinalSqlWithParams(processedSqlStr, paramValues);
|
||||
}
|
||||
break;
|
||||
case "SINGLE":
|
||||
result = executeSingleQuery(processedSqlStr, paramValues);
|
||||
finalExecutedSql = buildFinalSqlWithParams(processedSqlStr, paramValues);
|
||||
break;
|
||||
case "COUNT":
|
||||
result = executeCountQuery(processedSqlStr, paramValues);
|
||||
finalExecutedSql = buildFinalSqlWithParams(processedSqlStr, paramValues);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的结果类型: " + config.getResultType());
|
||||
}
|
||||
|
||||
return ApiExecutionResult.success(result, finalExecutedSql);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("SQL执行失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("SQL执行失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建包含参数值的最终SQL(用于日志和统计)
|
||||
* 注意:这只是展示用,实际执行使用PreparedStatement
|
||||
*/
|
||||
private String buildFinalSqlWithParams(String sql, List<Object> paramValues) {
|
||||
StringBuilder finalSql = new StringBuilder(sql);
|
||||
int paramIndex = 0;
|
||||
|
||||
// 替换 ? 为实际参数值(仅用于展示)
|
||||
for (int i = 0; i < finalSql.length() && paramIndex < paramValues.size(); i++) {
|
||||
if (finalSql.charAt(i) == '?') {
|
||||
Object value = paramValues.get(paramIndex++);
|
||||
String valueStr;
|
||||
|
||||
if (value == null) {
|
||||
valueStr = "NULL";
|
||||
} else if (value instanceof String) {
|
||||
valueStr = "'" + value + "'";
|
||||
} else if (value instanceof Boolean) {
|
||||
valueStr = ((Boolean) value) ? "1" : "0";
|
||||
} else {
|
||||
valueStr = String.valueOf(value);
|
||||
}
|
||||
|
||||
finalSql.replace(i, i + 1, valueStr);
|
||||
i += valueStr.length() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return finalSql.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理SQL模板(将 #{param} 转换为 ? 占位符,并提取参数值)
|
||||
*
|
||||
* @param sqlTemplate SQL模板
|
||||
* @param params 参数Map
|
||||
* @return 处理后的SQL和参数值列表
|
||||
*/
|
||||
private ProcessedSql processSqlTemplate(String sqlTemplate, Map<String, Object> params) {
|
||||
// 正则表达式匹配 #{paramName}
|
||||
Pattern pattern = Pattern.compile("#\\{(\\w+)\\}");
|
||||
Matcher matcher = pattern.matcher(sqlTemplate);
|
||||
|
||||
List<Object> paramValues = new ArrayList<>();
|
||||
StringBuffer processedSql = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String paramName = matcher.group(1);
|
||||
Object paramValue = params.get(paramName);
|
||||
|
||||
// 替换 #{param} 为 ?
|
||||
matcher.appendReplacement(processedSql, "?");
|
||||
|
||||
// 记录参数值
|
||||
paramValues.add(paramValue != null ? paramValue : null);
|
||||
}
|
||||
|
||||
matcher.appendTail(processedSql);
|
||||
|
||||
// 处理 WHERE 条件中的 IS NOT NULL THEN ... 逻辑
|
||||
String finalSql = processWhereConditions(processedSql.toString(), params);
|
||||
|
||||
return new ProcessedSql(finalSql, paramValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 WHERE 条件中的动态逻辑(IS NOT NULL THEN ...)
|
||||
* 例如:WHERE #{customerCode} IS NOT NULL THEN customer_code = #{customerCode}
|
||||
* 如果 customerCode 为 null,则移除该条件
|
||||
*
|
||||
* @param sql SQL语句
|
||||
* @param params 参数Map
|
||||
* @return 处理后的SQL
|
||||
*/
|
||||
private String processWhereConditions(String sql, Map<String, Object> params) {
|
||||
// 处理 #{param} IS NOT NULL THEN ... 的条件
|
||||
// 这种条件格式用于动态WHERE条件,只有当参数不为null时才生效
|
||||
Pattern pattern = Pattern.compile("AND\\s+#\\{(\\w+)\\}\\s+IS\\s+NOT\\s+NULL\\s+THEN\\s+(.+?)(?=\\s+AND|#|$)");
|
||||
Matcher matcher = pattern.matcher(sql);
|
||||
|
||||
StringBuffer result = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
String paramName = matcher.group(1);
|
||||
String condition = matcher.group(2);
|
||||
|
||||
Object paramValue = params.get(paramName);
|
||||
if (paramValue != null) {
|
||||
// 参数不为null,保留条件(替换 #{param} 为 ?)
|
||||
String processedCondition = condition.replaceAll("#\\{" + paramName + "\\}", "?");
|
||||
matcher.appendReplacement(result, "AND " + processedCondition);
|
||||
} else {
|
||||
// 参数为null,移除整个条件
|
||||
matcher.appendReplacement(result, "");
|
||||
}
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行分页查询(SQL Server语法:OFFSET FETCH)
|
||||
*
|
||||
* @param config API配置
|
||||
* @param sql SQL语句
|
||||
* @param paramValues 参数值列表
|
||||
* @return 分页结果
|
||||
*/
|
||||
/**
|
||||
* 执行分页查询(SQL Server 2008 R2兼容版本,使用ROW_NUMBER)
|
||||
*
|
||||
* @param config API配置
|
||||
* @param sql SQL语句
|
||||
* @param paramValues 参数值列表
|
||||
* @return 分页结果
|
||||
*/
|
||||
private TableDataInfo<Map<String, Object>> executePaginatedQuery(ErpApiConfig config, String sql, List<Object> paramValues) {
|
||||
// 从配置中获取分页参数名
|
||||
String pageParamName = config.getPageParamName() != null ? config.getPageParamName() : "pageNum";
|
||||
String sizeParamName = config.getSizeParamName() != null ? config.getSizeParamName() : "pageSize";
|
||||
|
||||
// 注意:paramValues是从SQL模板中的#{param}提取的,不包含分页参数
|
||||
// 分页参数需要通过其他方式传递,这里使用默认值
|
||||
// TODO: 改进参数传递,支持动态分页参数
|
||||
int pageNum = 1;
|
||||
int pageSize = 10;
|
||||
|
||||
// 查询总数
|
||||
String countSql = "SELECT COUNT(*) AS total FROM (" + sql + ") AS count_query";
|
||||
Long total = jdbcTemplate.queryForObject(countSql, paramValues.toArray(), Long.class);
|
||||
|
||||
// SQL Server 2008 R2兼容分页(使用ROW_NUMBER)
|
||||
// 注意:原始SQL不能有ORDER BY(ROW_NUMBER需要自己指定排序)
|
||||
int offset = (pageNum - 1) * pageSize;
|
||||
String paginatedSql = "SELECT * FROM (" +
|
||||
" SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS rn, * FROM (" +
|
||||
sql +
|
||||
") AS inner_query" +
|
||||
") AS numbered_query WHERE rn > " + offset + " AND rn <= " + (offset + pageSize);
|
||||
|
||||
// 查询数据
|
||||
List<Map<String, Object>> rows = jdbcTemplate.query(paginatedSql, paramValues.toArray(), new DynamicRowMapper());
|
||||
|
||||
// 移除ROW_NUMBER列(rn字段)
|
||||
rows.forEach(row -> row.remove("rn"));
|
||||
|
||||
return new TableDataInfo<>(rows, total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行列表查询
|
||||
*
|
||||
* @param sql SQL语句
|
||||
* @param paramValues 参数值列表
|
||||
* @return 列表结果
|
||||
*/
|
||||
private R<List<Map<String, Object>>> executeListQuery(String sql, List<Object> paramValues) {
|
||||
List<Map<String, Object>> rows = jdbcTemplate.query(sql, paramValues.toArray(), new DynamicRowMapper());
|
||||
return R.ok(rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单条查询
|
||||
*
|
||||
* @param sql SQL语句
|
||||
* @param paramValues 参数值列表
|
||||
* @return 单条结果
|
||||
*/
|
||||
private R<Map<String, Object>> executeSingleQuery(String sql, List<Object> paramValues) {
|
||||
List<Map<String, Object>> rows = jdbcTemplate.query(sql, paramValues.toArray(), new DynamicRowMapper());
|
||||
if (rows.isEmpty()) {
|
||||
return R.fail("未找到数据");
|
||||
}
|
||||
return R.ok(rows.get(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行计数查询
|
||||
*
|
||||
* @param sql SQL语句
|
||||
* @param paramValues 参数值列表
|
||||
* @return 计数结果
|
||||
*/
|
||||
private R<Long> executeCountQuery(String sql, List<Object> paramValues) {
|
||||
Long count = jdbcTemplate.queryForObject(sql, paramValues.toArray(), Long.class);
|
||||
return R.ok(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态RowMapper(将SQL结果映射为Map)
|
||||
*/
|
||||
private static class DynamicRowMapper implements RowMapper<Map<String, Object>> {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
ResultSetMetaData metaData = rs.getMetaData();
|
||||
int columnCount = metaData.getColumnCount();
|
||||
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
String columnName = metaData.getColumnLabel(i);
|
||||
Object value = rs.getObject(i);
|
||||
row.put(columnName, value);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理后的SQL和参数值
|
||||
*/
|
||||
private static class ProcessedSql {
|
||||
private final String sql;
|
||||
private final List<Object> paramValues;
|
||||
|
||||
public ProcessedSql(String sql, List<Object> paramValues) {
|
||||
this.sql = sql;
|
||||
this.paramValues = paramValues;
|
||||
}
|
||||
|
||||
public String getSql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
public List<Object> getParamValues() {
|
||||
return paramValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package org.hzhub.erp.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.hzhub.erp.common.page.TableDataInfo;
|
||||
import org.hzhub.erp.domain.entity.ErpApiConfig;
|
||||
import org.hzhub.erp.domain.entity.ErpApiParam;
|
||||
import org.hzhub.erp.domain.vo.ApiExecutionResult;
|
||||
import org.hzhub.erp.domain.vo.ApiTestResultVO;
|
||||
import org.hzhub.erp.domain.vo.ErpApiConfigVO;
|
||||
import org.hzhub.erp.mapper.ErpApiConfigMapper;
|
||||
import org.hzhub.erp.mapper.ErpApiParamMapper;
|
||||
import org.hzhub.erp.service.IErpApiService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* ERP动态API配置Service实现
|
||||
* 使用MySQL数据源存储配置信息
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
@Service
|
||||
@DS("master") // 使用MySQL数据源(master)
|
||||
@RequiredArgsConstructor
|
||||
public class ErpApiServiceImpl implements IErpApiService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ErpApiServiceImpl.class);
|
||||
|
||||
private final ErpApiConfigMapper apiConfigMapper;
|
||||
private final ErpApiParamMapper apiParamMapper;
|
||||
private final DynamicApiExecutor dynamicApiExecutor;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final ApiStatsRecorder apiStatsRecorder;
|
||||
|
||||
@Override
|
||||
public TableDataInfo<ErpApiConfigVO> queryApiConfigList(ErpApiConfigVO query, Integer pageNum, Integer pageSize) {
|
||||
Page<ErpApiConfig> page = new Page<>(pageNum, pageSize);
|
||||
|
||||
LambdaQueryWrapper<ErpApiConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
if (StrUtil.isNotBlank(query.getApiName())) {
|
||||
wrapper.like(ErpApiConfig::getApiName, query.getApiName());
|
||||
}
|
||||
if (StrUtil.isNotBlank(query.getApiPath())) {
|
||||
wrapper.like(ErpApiConfig::getApiPath, query.getApiPath());
|
||||
}
|
||||
if (query.getStatus() != null) {
|
||||
wrapper.eq(ErpApiConfig::getStatus, query.getStatus());
|
||||
}
|
||||
wrapper.orderByDesc(ErpApiConfig::getCreateTime);
|
||||
|
||||
IPage<ErpApiConfig> configPage = apiConfigMapper.selectPage(page, wrapper);
|
||||
|
||||
// 转换为VO
|
||||
List<ErpApiConfigVO> voList = configPage.getRecords().stream()
|
||||
.map(config -> BeanUtil.copyProperties(config, ErpApiConfigVO.class))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new TableDataInfo<>(voList, configPage.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErpApiConfig selectApiConfigById(Long apiId) {
|
||||
return apiConfigMapper.selectById(apiId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ErpApiParam> selectApiParamsByApiId(Long apiId) {
|
||||
return apiParamMapper.selectByApiId(apiId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErpApiConfig selectApiConfigByPath(String apiPath, String apiMethod, String apiVersion) {
|
||||
return apiConfigMapper.selectByPathAndMethod(apiPath, apiMethod, apiVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int insertApiConfig(ErpApiConfig config) {
|
||||
return apiConfigMapper.insert(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int updateApiConfig(ErpApiConfig config) {
|
||||
return apiConfigMapper.updateById(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteApiConfigByIds(Long[] apiIds) {
|
||||
int count = 0;
|
||||
for (Long apiId : apiIds) {
|
||||
count += apiConfigMapper.deleteById(apiId);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateApiStatus(ErpApiConfig config) {
|
||||
ErpApiConfig updateConfig = new ErpApiConfig();
|
||||
updateConfig.setApiId(config.getApiId());
|
||||
updateConfig.setStatus(config.getStatus());
|
||||
return apiConfigMapper.updateById(updateConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void importFromTable(String[] tableNames, String dataSource) {
|
||||
// TODO: 实现从表导入功能(Phase 2)
|
||||
log.info("从表导入功能待实现: tableNames={}, dataSource={}", Arrays.toString(tableNames), dataSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncTableStructure(Long apiId) {
|
||||
// TODO: 实现同步表结构功能(Phase 4)
|
||||
log.info("同步表结构功能待实现: apiId={}", apiId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiTestResultVO testApi(Long apiId, Map<String, Object> testParams, String clientIp, String userId) {
|
||||
ErpApiConfig config = selectApiConfigById(apiId);
|
||||
if (config == null) {
|
||||
throw new RuntimeException("API配置不存在");
|
||||
}
|
||||
|
||||
ApiTestResultVO result = new ApiTestResultVO();
|
||||
result.setApiPath(config.getApiPath());
|
||||
result.setTestMethod(config.getApiMethod());
|
||||
result.setRequestParams(testParams);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
ApiExecutionResult executionResult = dynamicApiExecutor.execute(config, testParams);
|
||||
Object data = executionResult.getData();
|
||||
String executedSql = executionResult.getExecutedSql();
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
result.setSuccess(true);
|
||||
result.setData(data);
|
||||
result.setExecutionTime(executionTime);
|
||||
result.setExecutedSql(executedSql);
|
||||
|
||||
log.info("API测试成功: apiId={}, executionTime={}ms", apiId, executionTime);
|
||||
|
||||
// 记录成功统计(异步记录,不影响响应)
|
||||
try {
|
||||
String callParamsJson = testParams.toString();
|
||||
apiStatsRecorder.recordSuccess(apiId, callParamsJson, executedSql,
|
||||
executionTime, clientIp, userId);
|
||||
} catch (Exception statsError) {
|
||||
log.warn("统计记录失败(不影响API响应): {}", statsError.getMessage());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
result.setSuccess(false);
|
||||
result.setErrorMessage(e.getMessage());
|
||||
|
||||
// 获取错误堆栈
|
||||
StringBuilder stackTrace = new StringBuilder();
|
||||
for (StackTraceElement element : e.getStackTrace()) {
|
||||
stackTrace.append(element.toString()).append("\n");
|
||||
}
|
||||
result.setErrorStack(stackTrace.toString());
|
||||
|
||||
log.error("API测试失败: apiId={}, error={}", apiId, e.getMessage(), e);
|
||||
|
||||
// 记录错误统计
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
String executedSql = config.getSqlTemplate();
|
||||
try {
|
||||
String callParamsJson = testParams.toString();
|
||||
apiStatsRecorder.recordError(apiId, callParamsJson, executedSql,
|
||||
executionTime, e.getMessage(),
|
||||
stackTrace.toString(), clientIp, userId);
|
||||
} catch (Exception statsError) {
|
||||
log.warn("统计记录失败(不影响API响应): {}", statsError.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> generateApiDoc(Long apiId) {
|
||||
// TODO: 实现API文档生成(Phase 3)
|
||||
ErpApiConfig config = selectApiConfigById(apiId);
|
||||
List<ErpApiParam> params = selectApiParamsByApiId(apiId);
|
||||
|
||||
Map<String, String> docMap = new HashMap<>();
|
||||
docMap.put("basic", generateBasicInfo(config));
|
||||
docMap.put("params", generateParamsInfo(params));
|
||||
docMap.put("sql", config.getSqlTemplate());
|
||||
|
||||
return docMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getApiStats(Long apiId, String startTime, String endTime) {
|
||||
// 查询统计数据
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 构建SQL查询
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT ");
|
||||
sql.append("COUNT(*) AS totalCalls, ");
|
||||
sql.append("SUM(CASE WHEN call_status = 'SUCCESS' THEN 1 ELSE 0 END) AS successCalls, ");
|
||||
sql.append("SUM(CASE WHEN call_status = 'ERROR' THEN 1 ELSE 0 END) AS errorCalls, ");
|
||||
sql.append("AVG(response_time) AS avgResponseTime, ");
|
||||
sql.append("MAX(response_time) AS maxResponseTime, ");
|
||||
sql.append("MIN(response_time) AS minResponseTime ");
|
||||
sql.append("FROM erp_api_stats ");
|
||||
sql.append("WHERE api_id = ? ");
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(apiId);
|
||||
|
||||
if (startTime != null && !startTime.isEmpty()) {
|
||||
sql.append("AND call_time >= ? ");
|
||||
params.add(startTime);
|
||||
}
|
||||
|
||||
if (endTime != null && !endTime.isEmpty()) {
|
||||
sql.append("AND call_time <= ? ");
|
||||
params.add(endTime);
|
||||
}
|
||||
|
||||
log.info("查询统计数据: apiId={}, SQL={}, params={}", apiId, sql.toString(), params);
|
||||
|
||||
// 执行查询
|
||||
Map<String, Object> result = jdbcTemplate.queryForMap(sql.toString(), params.toArray());
|
||||
|
||||
log.info("查询结果: {}", result);
|
||||
|
||||
// 处理类型转换(MySQL返回类型可能不同)
|
||||
// COUNT返回Long或BigDecimal
|
||||
Object totalCallsObj = result.get("totalCalls");
|
||||
stats.put("totalCalls", convertToLong(totalCallsObj));
|
||||
|
||||
Object successCallsObj = result.get("successCalls");
|
||||
stats.put("successCalls", convertToLong(successCallsObj));
|
||||
|
||||
Object errorCallsObj = result.get("errorCalls");
|
||||
stats.put("errorCalls", convertToLong(errorCallsObj));
|
||||
|
||||
// 响应时间处理(可能为null)
|
||||
Object avgTime = result.get("avgResponseTime");
|
||||
stats.put("avgResponseTime", avgTime != null ? convertToLong(avgTime) : 0);
|
||||
|
||||
Object maxTime = result.get("maxResponseTime");
|
||||
stats.put("maxResponseTime", maxTime != null ? convertToLong(maxTime) : 0);
|
||||
|
||||
Object minTime = result.get("minResponseTime");
|
||||
stats.put("minResponseTime", minTime != null ? convertToLong(minTime) : 0);
|
||||
|
||||
// 计算错误率
|
||||
Long totalCalls = (Long) stats.get("totalCalls");
|
||||
Long errorCalls = (Long) stats.get("errorCalls");
|
||||
Double errorRate = totalCalls > 0 ? (errorCalls * 100.0 / totalCalls) : 0.0;
|
||||
stats.put("errorRate", errorRate);
|
||||
|
||||
log.info("统计数据返回: totalCalls={}, successCalls={}, errorCalls={}, avgTime={}ms",
|
||||
totalCalls, stats.get("successCalls"), errorCalls, stats.get("avgResponseTime"));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("查询统计数据失败: apiId={}, error={}", apiId, e.getMessage(), e);
|
||||
// 返回默认值
|
||||
stats.put("totalCalls", 0);
|
||||
stats.put("successCalls", 0);
|
||||
stats.put("errorCalls", 0);
|
||||
stats.put("avgResponseTime", 0);
|
||||
stats.put("maxResponseTime", 0);
|
||||
stats.put("minResponseTime", 0);
|
||||
stats.put("errorRate", 0.0);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据库返回的数值转换为Long
|
||||
* MySQL可能返回Long、BigDecimal或Integer
|
||||
*/
|
||||
private Long convertToLong(Object value) {
|
||||
if (value == null) {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
if (value instanceof Long) {
|
||||
return (Long) value;
|
||||
} else if (value instanceof java.math.BigDecimal) {
|
||||
return ((java.math.BigDecimal) value).longValue();
|
||||
} else if (value instanceof Integer) {
|
||||
return ((Integer) value).longValue();
|
||||
} else if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
} else {
|
||||
log.warn("无法转换类型: {} -> {}", value.getClass().getName(), value);
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getApiErrorLog(Long apiId, Integer limit) {
|
||||
// 查询错误日志
|
||||
try {
|
||||
String sql = "SELECT stats_id, api_id, call_time, call_params, response_time, " +
|
||||
"call_status, error_message, error_stack, client_ip, user_id " +
|
||||
"FROM erp_api_stats " +
|
||||
"WHERE api_id = ? AND call_status = 'ERROR' " +
|
||||
"ORDER BY call_time DESC " +
|
||||
"LIMIT ?";
|
||||
|
||||
List<Map<String, Object>> logs = jdbcTemplate.queryForList(sql, apiId, limit != null ? limit : 10);
|
||||
return logs;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("查询错误日志失败: {}", e.getMessage(), e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearApiCache(Long apiId) {
|
||||
// TODO: 实现缓存清除(Phase 4)
|
||||
log.info("缓存清除功能待实现: apiId={}", apiId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成基本信息文档
|
||||
*/
|
||||
private String generateBasicInfo(ErpApiConfig config) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("API名称: ").append(config.getApiName()).append("\n");
|
||||
sb.append("API路径: ").append(config.getApiPath()).append("\n");
|
||||
sb.append("HTTP方法: ").append(config.getApiMethod()).append("\n");
|
||||
sb.append("描述: ").append(config.getApiDesc()).append("\n");
|
||||
sb.append("版本: ").append(config.getApiVersion()).append("\n");
|
||||
sb.append("结果类型: ").append(config.getResultType()).append("\n");
|
||||
sb.append("支持分页: ").append(config.getSupportPagination() == 1 ? "是" : "否").append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成参数信息文档
|
||||
*/
|
||||
private String generateParamsInfo(List<ErpApiParam> params) {
|
||||
if (params == null || params.isEmpty()) {
|
||||
return "无参数";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("参数列表:\n");
|
||||
for (ErpApiParam param : params) {
|
||||
sb.append("- ").append(param.getParamName())
|
||||
.append(" (").append(param.getParamType()).append(")")
|
||||
.append(": ").append(param.getParamDesc())
|
||||
.append(param.getIsRequired() == 1 ? " [必填]" : "")
|
||||
.append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.hzhub.erp.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 参数类型转换器
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
public class ParamTypeConverter {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ParamTypeConverter.class);
|
||||
|
||||
/**
|
||||
* 根据参数类型转换参数值
|
||||
*
|
||||
* @param paramType 参数类型(String/Integer/Long/Date/Boolean)
|
||||
* @param value 参数值
|
||||
* @return 转换后的值
|
||||
*/
|
||||
public static Object convert(String paramType, Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (paramType.toUpperCase()) {
|
||||
case "STRING":
|
||||
return value.toString();
|
||||
|
||||
case "INTEGER":
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
return Integer.parseInt(value.toString());
|
||||
|
||||
case "LONG":
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
return Long.parseLong(value.toString());
|
||||
|
||||
case "DOUBLE":
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
}
|
||||
return Double.parseDouble(value.toString());
|
||||
|
||||
case "DATE":
|
||||
return parseDate(value.toString());
|
||||
|
||||
case "DATETIME":
|
||||
return parseDateTime(value.toString());
|
||||
|
||||
case "BOOLEAN":
|
||||
if (value instanceof Boolean) {
|
||||
return value;
|
||||
}
|
||||
return Boolean.parseBoolean(value.toString());
|
||||
|
||||
default:
|
||||
log.warn("未知的参数类型: {}, 返回原值", paramType);
|
||||
return value;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("参数类型转换失败: paramType={}, value={}, error={}", paramType, value, e.getMessage());
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期(yyyy-MM-dd)
|
||||
*
|
||||
* @param dateStr 日期字符串
|
||||
* @return Date对象
|
||||
*/
|
||||
private static Date parseDate(String dateStr) throws ParseException {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
return sdf.parse(dateStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期时间(yyyy-MM-dd HH:mm:ss)
|
||||
*
|
||||
* @param dateTimeStr 日期时间字符串
|
||||
* @return LocalDateTime对象
|
||||
*/
|
||||
private static LocalDateTime parseDateTime(String dateTimeStr) {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
return LocalDateTime.parse(dateTimeStr, formatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是有效的参数类型
|
||||
*
|
||||
* @param paramType 参数类型
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean isValidType(String paramType) {
|
||||
if (paramType == null || paramType.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String upperType = paramType.toUpperCase();
|
||||
return upperType.equals("STRING")
|
||||
|| upperType.equals("INTEGER")
|
||||
|| upperType.equals("LONG")
|
||||
|| upperType.equals("DOUBLE")
|
||||
|| upperType.equals("DATE")
|
||||
|| upperType.equals("DATETIME")
|
||||
|| upperType.equals("BOOLEAN");
|
||||
}
|
||||
}
|
||||
118
hzhub-erp/src/main/java/org/hzhub/erp/util/SqlValidator.java
Normal file
118
hzhub-erp/src/main/java/org/hzhub/erp/util/SqlValidator.java
Normal file
@@ -0,0 +1,118 @@
|
||||
package org.hzhub.erp.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* SQL安全验证工具
|
||||
*
|
||||
* @author HZHub Team
|
||||
*/
|
||||
public class SqlValidator {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SqlValidator.class);
|
||||
|
||||
/**
|
||||
* SQL白名单关键字(允许的SQL关键字)
|
||||
*/
|
||||
private static final Set<String> ALLOWED_KEYWORDS = new HashSet<>(Arrays.asList(
|
||||
"SELECT", "FROM", "WHERE", "AND", "OR", "ORDER", "BY", "GROUP", "HAVING",
|
||||
"OFFSET", "FETCH", "NEXT", "ROWS", "ONLY", "AS", "COUNT", "SUM", "AVG",
|
||||
"MIN", "MAX", "DISTINCT", "TOP", "INNER", "LEFT", "RIGHT", "JOIN", "ON",
|
||||
"IS", "NULL", "NOT", "IN", "LIKE", "BETWEEN", "EXISTS", "CASE", "WHEN",
|
||||
"THEN", "ELSE", "END", "UNION", "ALL", "WITH", "OVER", "PARTITION"
|
||||
));
|
||||
|
||||
/**
|
||||
* SQL危险关键字(禁止的SQL关键字)
|
||||
*/
|
||||
private static final Set<String> DANGEROUS_KEYWORDS = new HashSet<>(Arrays.asList(
|
||||
"DROP", "DELETE", "TRUNCATE", "ALTER", "CREATE", "GRANT", "REVOKE",
|
||||
"INSERT", "UPDATE", "EXEC", "EXECUTE", "MERGE", "CALL"
|
||||
));
|
||||
|
||||
/**
|
||||
* 验证SQL安全性
|
||||
*
|
||||
* @param sql SQL语句
|
||||
* @return 是否安全
|
||||
*/
|
||||
public static boolean validate(String sql) {
|
||||
if (sql == null || sql.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 提取SQL关键字
|
||||
String[] words = sql.toUpperCase().split("\\s+");
|
||||
|
||||
// 检查危险关键字
|
||||
for (String word : words) {
|
||||
if (DANGEROUS_KEYWORDS.contains(word)) {
|
||||
log.error("SQL包含危险关键字: {}", word);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否以SELECT开头(只允许查询语句)
|
||||
String upperSql = sql.trim().toUpperCase();
|
||||
if (!upperSql.startsWith("SELECT")) {
|
||||
log.error("SQL不是查询语句,必须以SELECT开头");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查分号(防止多条SQL注入)
|
||||
if (sql.contains(";") && !sql.trim().endsWith(";")) {
|
||||
log.error("SQL包含多个语句(分号),可能存在注入风险");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含参数占位符
|
||||
*
|
||||
* @param sql SQL语句
|
||||
* @return 是否包含参数占位符 #{paramName}
|
||||
*/
|
||||
public static boolean containsParams(String sql) {
|
||||
return sql != null && sql.contains("#{") && sql.contains("}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取参数名称列表
|
||||
*
|
||||
* @param sql SQL语句
|
||||
* @return 参数名称列表
|
||||
*/
|
||||
public static Set<String> extractParamNames(String sql) {
|
||||
Set<String> paramNames = new HashSet<>();
|
||||
|
||||
if (sql == null) {
|
||||
return paramNames;
|
||||
}
|
||||
|
||||
// 提取 #{paramName} 中的参数名
|
||||
int start = 0;
|
||||
while (start < sql.length()) {
|
||||
int begin = sql.indexOf("#{", start);
|
||||
if (begin == -1) {
|
||||
break;
|
||||
}
|
||||
int end = sql.indexOf("}", begin);
|
||||
if (end == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
String paramName = sql.substring(begin + 2, end).trim();
|
||||
paramNames.add(paramName);
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
return paramNames;
|
||||
}
|
||||
}
|
||||
17
hzhub-erp/src/main/resources/application-dev.yml
Normal file
17
hzhub-erp/src/main/resources/application-dev.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
# MyBatis SQL 日志输出
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
# 日志级别
|
||||
logging:
|
||||
level:
|
||||
org.hzhub.erp: debug
|
||||
com.zaxxer.hikari: debug
|
||||
|
||||
# 开发环境数据源(覆盖 application.yml 中的占位符)
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:sqlserver://192.168.120.10:8042;databaseName=DMPF_HY;encrypt=false;trustServerCertificate=true;loginTimeout=10
|
||||
username: aiuser
|
||||
password: aiuser123
|
||||
@@ -4,43 +4,48 @@ server:
|
||||
spring:
|
||||
application:
|
||||
name: hzhub-erp
|
||||
|
||||
# 多数据源配置
|
||||
datasource:
|
||||
# 主数据源 - SQL Server 2008 R2 (ERP直连)
|
||||
primary:
|
||||
url: jdbc:sqlserver://192.168.x.x:1433;database=ERP;encrypt=false;trustServerCertificate=true
|
||||
username: ${ERP_DB_USERNAME:sa}
|
||||
password: ${ERP_DB_PASSWORD:}
|
||||
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
|
||||
# secondary数据源 - MySQL (预留)
|
||||
secondary:
|
||||
url: jdbc:mysql://localhost:3306/hzhub_erp?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 5
|
||||
|
||||
# MyBatis Plus配置
|
||||
datasource:
|
||||
dynamic:
|
||||
primary: master # 设置默认数据源为master(MySQL)
|
||||
strict: false # 允许非严格匹配
|
||||
datasource:
|
||||
# MySQL数据源 - 用于存储API配置、参数、统计信息
|
||||
master:
|
||||
url: jdbc:mysql://${MYSQL_HOST:192.168.120.60}:${MYSQL_PORT:3306}/hzhub?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: ${MYSQL_USERNAME:root}
|
||||
password: ${MYSQL_PASSWORD:hzhub123}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# SQL Server数据源 - 用于执行ERP动态SQL查询(只读)
|
||||
erp:
|
||||
url: jdbc:sqlserver://${ERP_DB_HOST:192.168.120.10}:${ERP_DB_PORT:8042};databaseName=${ERP_DB_NAME:DMPF_HY};encrypt=false;trustServerCertificate=true
|
||||
username: ${ERP_DB_USERNAME:aiuser}
|
||||
password: ${ERP_DB_PASSWORD:aiuser123}
|
||||
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
serialization:
|
||||
indent_output: false
|
||||
fail_on_empty_beans: false
|
||||
deserialization:
|
||||
fail_on_unknown_properties: false
|
||||
|
||||
profiles:
|
||||
active: dev
|
||||
|
||||
# MyBatis-Plus 配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
map-underscore-to-camel-case: true
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:mapper/**/*.xml
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
|
||||
# Sa-Token配置
|
||||
# Sa-Token 配置(JWT secret 需与 hzhub-ai 一致以支持跨服务 Token 验证)
|
||||
sa-token:
|
||||
token-name: Authorization
|
||||
timeout: 86400
|
||||
@@ -49,9 +54,22 @@ sa-token:
|
||||
is-share: false
|
||||
token-style: uuid
|
||||
is-log: false
|
||||
jwt-secret-key: ${ERP_JWT_SECRET:abcdefghijklmnopqrstuvwxyz}
|
||||
token-prefix: "Bearer"
|
||||
is-read-header: true
|
||||
is-read-cookie: false
|
||||
# 开发阶段:关闭认证检查
|
||||
is-read-body: false
|
||||
check-id-token: false
|
||||
is-token-header: false
|
||||
is-token-cookie: false
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.foshanhuiya.erp: debug
|
||||
org.springframework.jdbc: debug
|
||||
# Actuator 健康检查
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
46
hzhub-erp/src/main/resources/mapper/ErpApiConfigMapper.xml
Normal file
46
hzhub-erp/src/main/resources/mapper/ErpApiConfigMapper.xml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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="org.hzhub.erp.mapper.ErpApiConfigMapper">
|
||||
|
||||
<resultMap id="ErpApiConfigResult" type="org.hzhub.erp.domain.entity.ErpApiConfig">
|
||||
<id property="apiId" column="api_id"/>
|
||||
<result property="apiName" column="api_name"/>
|
||||
<result property="apiPath" column="api_path"/>
|
||||
<result property="apiMethod" column="api_method"/>
|
||||
<result property="apiDesc" column="api_desc"/>
|
||||
<result property="apiVersion" column="api_version"/>
|
||||
<result property="dataSource" column="data_source"/>
|
||||
<result property="sqlTemplate" column="sql_template"/>
|
||||
<result property="resultType" column="result_type"/>
|
||||
<result property="supportPagination" column="support_pagination"/>
|
||||
<result property="pageParamName" column="page_param_name"/>
|
||||
<result property="sizeParamName" column="size_param_name"/>
|
||||
<result property="requireAuth" column="require_auth"/>
|
||||
<result property="permissionCode" column="permission_code"/>
|
||||
<result property="enableCache" column="enable_cache"/>
|
||||
<result property="cacheKeyTemplate" column="cache_key_template"/>
|
||||
<result property="cacheTtl" column="cache_ttl"/>
|
||||
<result property="sourceTable" column="source_table"/>
|
||||
<result property="sourceTableComment" column="source_table_comment"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
<result property="updateBy" column="update_by"/>
|
||||
<result property="remark" column="remark"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectErpApiConfigVo">
|
||||
select api_id, api_name, api_path, api_method, api_desc, api_version, data_source,
|
||||
sql_template, result_type, support_pagination, page_param_name, size_param_name,
|
||||
require_auth, permission_code, enable_cache, cache_key_template, cache_ttl,
|
||||
source_table, source_table_comment, status, create_time, update_time, create_by, update_by, remark
|
||||
from erp_api_config
|
||||
</sql>
|
||||
|
||||
<select id="selectByPathAndMethod" parameterType="String" resultMap="ErpApiConfigResult">
|
||||
<include refid="selectErpApiConfigVo"/>
|
||||
where api_path = #{apiPath} and api_method = #{apiMethod} and api_version = #{apiVersion} and status = 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
46
hzhub-erp/src/main/resources/mapper/ErpApiParamMapper.xml
Normal file
46
hzhub-erp/src/main/resources/mapper/ErpApiParamMapper.xml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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="org.hzhub.erp.mapper.ErpApiParamMapper">
|
||||
|
||||
<resultMap id="ErpApiParamResult" type="org.hzhub.erp.domain.entity.ErpApiParam">
|
||||
<id property="paramId" column="param_id"/>
|
||||
<result property="apiId" column="api_id"/>
|
||||
<result property="paramName" column="param_name"/>
|
||||
<result property="paramDesc" column="param_desc"/>
|
||||
<result property="paramType" column="param_type"/>
|
||||
<result property="paramPosition" column="param_position"/>
|
||||
<result property="isRequired" column="is_required"/>
|
||||
<result property="defaultValue" column="default_value"/>
|
||||
<result property="sqlParamName" column="sql_param_name"/>
|
||||
<result property="sort" column="sort"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectErpApiParamVo">
|
||||
select param_id, api_id, param_name, param_desc, param_type, param_position,
|
||||
is_required, default_value, sql_param_name, sort, create_time, update_time
|
||||
from erp_api_param
|
||||
</sql>
|
||||
|
||||
<select id="selectByApiId" parameterType="Long" resultMap="ErpApiParamResult">
|
||||
<include refid="selectErpApiParamVo"/>
|
||||
where api_id = #{apiId}
|
||||
order by sort asc
|
||||
</select>
|
||||
|
||||
<insert id="batchInsert" parameterType="java.util.List">
|
||||
insert into erp_api_param(api_id, param_name, param_desc, param_type, param_position,
|
||||
is_required, default_value, sql_param_name, sort, create_time)
|
||||
values
|
||||
<foreach collection="params" item="param" separator=",">
|
||||
(#{param.apiId}, #{param.paramName}, #{param.paramDesc}, #{param.paramType}, #{param.paramPosition},
|
||||
#{param.isRequired}, #{param.defaultValue}, #{param.sqlParamName}, #{param.sort}, now())
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<delete id="deleteByApiId" parameterType="Long">
|
||||
delete from erp_api_param where api_id = #{apiId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
63
hzhub-erp/src/main/resources/mapper/ErpApiStatsMapper.xml
Normal file
63
hzhub-erp/src/main/resources/mapper/ErpApiStatsMapper.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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="org.hzhub.erp.mapper.ErpApiStatsMapper">
|
||||
|
||||
<resultMap id="ErpApiStatsResult" type="org.hzhub.erp.domain.entity.ErpApiStats">
|
||||
<id property="statsId" column="stats_id"/>
|
||||
<result property="apiId" column="api_id"/>
|
||||
<result property="callTime" column="call_time"/>
|
||||
<result property="callParams" column="call_params"/>
|
||||
<result property="responseTime" column="response_time"/>
|
||||
<result property="callStatus" column="call_status"/>
|
||||
<result property="errorMessage" column="error_message"/>
|
||||
<result property="errorStack" column="error_stack"/>
|
||||
<result property="clientIp" column="client_ip"/>
|
||||
<result property="userId" column="user_id"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectErpApiStatsVo">
|
||||
select stats_id, api_id, call_time, call_params, response_time, call_status,
|
||||
error_message, error_stack, client_ip, user_id, create_time
|
||||
from erp_api_stats
|
||||
</sql>
|
||||
|
||||
<select id="selectByApiIdAndTime" resultMap="ErpApiStatsResult">
|
||||
<include refid="selectErpApiStatsVo"/>
|
||||
where api_id = #{apiId}
|
||||
and call_time between #{startTime} and #{endTime}
|
||||
order by call_time desc
|
||||
</select>
|
||||
|
||||
<select id="selectErrorLogByApiId" resultMap="ErpApiStatsResult">
|
||||
<include refid="selectErpApiStatsVo"/>
|
||||
where api_id = #{apiId}
|
||||
and call_status = 'ERROR'
|
||||
order by call_time desc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="countByApiId" resultType="Long">
|
||||
select count(*)
|
||||
from erp_api_stats
|
||||
where api_id = #{apiId}
|
||||
and call_time between #{startTime} and #{endTime}
|
||||
</select>
|
||||
|
||||
<select id="avgResponseTimeByApiId" resultType="Integer">
|
||||
select avg(response_time)
|
||||
from erp_api_stats
|
||||
where api_id = #{apiId}
|
||||
and call_time between #{startTime} and #{endTime}
|
||||
and call_status = 'SUCCESS'
|
||||
</select>
|
||||
|
||||
<select id="countErrorByApiId" resultType="Long">
|
||||
select count(*)
|
||||
from erp_api_stats
|
||||
where api_id = #{apiId}
|
||||
and call_time between #{startTime} and #{endTime}
|
||||
and call_status = 'ERROR'
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user