Compare commits

..

11 Commits

40 changed files with 881 additions and 146 deletions

1
.gitignore vendored
View File

@@ -82,3 +82,4 @@ Icon
Network Trash Folder
Temporary Items
.apdisk
*.db

20
.idea/compiler.xml generated
View File

@@ -7,8 +7,28 @@
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
</profile>
<profile name="Annotation profile for spring-boot-server-template" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<option name="mapstruct.defaultComponentModel" value="spring" />
<option name="mapstruct.defaultInjectionStrategy" value="constructor" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct-processor/1.6.3/mapstruct-processor-1.6.3.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct/1.6.3/mapstruct-1.6.3.jar" />
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/unknown/lombok-unknown.jar" />
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok-mapstruct-binding/0.2.0/lombok-mapstruct-binding-0.2.0.jar" />
</processorPath>
<module name="spring-boot-server-template" />
<module name="spring-boot-apijson-server" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="spring-boot-apijson-server" options="-Amapstruct.defaultComponentModel=spring -Amapstruct.defaultInjectionStrategy=constructor" />
<module name="spring-boot-server-template" options="-Amapstruct.defaultComponentModel=spring -Amapstruct.defaultInjectionStrategy=constructor" />
</option>
</component>
</project>

12
.idea/dataSources.xml generated
View File

@@ -26,5 +26,17 @@
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="database" uuid="894c61bb-b6fb-4153-94dc-97175dd7fd95">
<driver-ref>h2.unified</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.h2.Driver</jdbc-driver>
<jdbc-url>jdbc:h2:$PROJECT_DIR$/database</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -0,0 +1,15 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="NullableProblems" enabled="false" level="WARNING" enabled_by_default="false">
<option name="REPORT_NULLABLE_METHOD_OVERRIDES_NOTNULL" value="true" />
<option name="REPORT_NOT_ANNOTATED_METHOD_OVERRIDES_NOTNULL" value="true" />
<option name="REPORT_NOTNULL_PARAMETER_OVERRIDES_NULLABLE" value="true" />
<option name="REPORT_NOT_ANNOTATED_PARAMETER_OVERRIDES_NOTNULL" value="true" />
<option name="REPORT_NOT_ANNOTATED_GETTER" value="true" />
<option name="REPORT_NOT_ANNOTATED_SETTER_PARAMETER" value="true" />
<option name="REPORT_ANNOTATION_NOT_PROPAGATED_TO_OVERRIDERS" value="true" />
<option name="REPORT_NULLS_PASSED_TO_NON_ANNOTATED_METHOD" value="true" />
</inspection_tool>
</profile>
</component>

View File

@@ -11,6 +11,11 @@
<option name="name" value="jitpack.io" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="lanyuanxiaoyao-maven-central" />
<option name="name" value="lanyuanxiaoyao-maven-central" />
<option name="url" value="https://maven.lanyuanxiaoyao.com/central" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="central" />
@@ -26,5 +31,10 @@
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.lanyuanxiaoyao.com/central" />
</remote-repository>
</component>
</project>

2
.idea/modules.xml generated
View File

@@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/spring-boot-apijson-server.iml" filepath="$PROJECT_DIR$/spring-boot-apijson-server.iml" />
<module fileurl="file://$PROJECT_DIR$/spring-boot-server-template.iml" filepath="$PROJECT_DIR$/spring-boot-server-template.iml" />
</modules>
</component>
</project>

View File

@@ -11,6 +11,7 @@
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "4.x",
"licia": "^1.48.0",
"vue": "^3.5.13",
"vue-router": "4"
},

8
client/pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
ant-design-vue:
specifier: 4.x
version: 4.2.6(vue@3.5.13)
licia:
specifier: ^1.48.0
version: 1.48.0
vue:
specifier: ^3.5.13
version: 3.5.13
@@ -681,6 +684,9 @@ packages:
libphonenumber-js@1.12.5:
resolution: {integrity: sha512-DOjiaVjjSmap12ztyb4QgoFmUe/GbgnEXHu+R7iowk0lzDIjScvPAm8cK9RYTEobbRb0OPlwlZUGTTJPJg13Kw==}
licia@1.48.0:
resolution: {integrity: sha512-bBWiT5CSdEtwuAHiYTJ74yItCjIFdHi4xiFk6BRDfKa+sdCpkUHp69YKb5udNOJlHDzFjNjcMgNZ/+wQIHrB8A==}
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
@@ -1489,6 +1495,8 @@ snapshots:
libphonenumber-js@1.12.5: {}
licia@1.48.0: {}
lodash-es@4.17.21: {}
lodash@4.17.21: {}

View File

@@ -0,0 +1,19 @@
<script setup>
import {ref} from "vue";
import {Button} from "ant-design-vue";
const message = ref('Vue Flow')
</script>
<template>
<div class="flow"></div>
<Button>{{ message}}</Button>
</template>
<style scoped>
.flow {
width: 10px;
height: 10px;
background-color: red;
}
</style>

View File

@@ -1,6 +1,6 @@
import {createApp} from 'vue'
import {createMemoryHistory, createRouter, createWebHistory} from 'vue-router'
import App from './App.vue'
import {createRouter, createWebHistory} from 'vue-router'
import App from '@/App.vue'
import {Layout, Menu} from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
@@ -14,23 +14,28 @@ createApp(App)
{
name: 'home',
path: '/home',
component: () => import('./views/Home.vue'),
component: () => import('@/views/Home.vue'),
},
{
name: 'management',
path: '/management',
component: () => import('./views/management/Index.vue'),
component: () => import('@/views/management/Index.vue'),
redirect: '/management/overview',
children: [
{
name: 'overview',
path: 'overview',
component: () => import('./views/management/Overview.vue'),
component: () => import('@/views/management/Overview.vue'),
},
{
name: 'organization',
path: 'organization',
component: () => import('@/views/management/Organization.vue'),
},
{
name: 'setting',
path: 'setting',
component: () => import('./views/management/Setting.vue'),
component: () => import('@/views/management/Setting.vue'),
},
],
},

View File

@@ -1,19 +1,54 @@
import {isArr} from "licia";
const information = {
debug: true,
baseUrl: 'http://localhost'
debug: true,
baseUrl: 'http://localhost:8080',
}
export function amisRender(target, amisJson) {
let amisJsonObject = amisJson(information)
if (information.debug) {
console.log(amisJsonObject)
let amisJsonObject = amisJson(information)
if (information.debug) {
console.log(amisJsonObject)
}
amisRequire('amis/embed').embed(
target,
amisJsonObject,
information,
{
theme: 'antd',
enableAMISDebug: information.debug,
},
)
}
function parseEdges(edges) {
if (isArr(edges)) {
}
}
export function amisElideGraphQLAdaptor(payload, response, api, context) {
// console.log(payload, response, api, context)
let result = []
console.log(payload)
if (payload.data) {
let items = payload.data[Object.keys(payload.data)[0]]['edges']
for (let item of items) {
result.push(item.node)
}
amisRequire('amis/embed').embed(
target,
amisJsonObject,
information,
{
theme: 'antd'
}
)
}
}
return result
}
export function amisElideJsonapiAdaptor(payload, response, api, context) {
let result = []
if (payload.data && isArr(payload.data)) {
for (let item of payload.data) {
result.push({
...item,
...item['attributes'],
})
}
}
return result
}

View File

@@ -1,6 +1,7 @@
<script setup>
import {onMounted} from 'vue'
import {amisRender} from '../utils.js'
import {createApp, onMounted} from 'vue'
import {amisRender} from '@/utils.js'
import Flow from "@/components/Flow.vue";
const toastMessage = () => {
alert('click in vue')
@@ -29,6 +30,12 @@ onMounted(() => {
]
}
}
},
{
type: 'custom',
onMount: (dom, value, onChange, props) => {
createApp(Flow).mount(dom)
}
}
]
}

View File

@@ -1,5 +1,8 @@
<script setup>
import {ref, watch} from 'vue'
import {
ref,
watch,
} from 'vue'
import {useRoute} from 'vue-router'
const route = useRoute();
@@ -13,13 +16,18 @@ const menus = [
key: 'system',
name: '系统管理',
children: [
{
key: 'organization',
path: '/management/organization',
name: '组织架构',
},
{
key: 'setting',
path: '/management/setting',
name: '设置',
}
]
}
},
],
},
]
const sideNavSelected = ref(['overview'])
const openKeys = ref([]) // 控制展开的子菜单
@@ -65,19 +73,21 @@ watch(() => route.path, () => {
v-model:openKeys="openKeys"
mode="inline"
>
<a-menu-item key="overview">
<router-link to="/management/overview">概览</router-link>
</a-menu-item>
<a-sub-menu key="system">
<template #title>
<div v-for="menu in menus" :key="menu.key">
<a-sub-menu v-if="menu.children" :key="menu.key">
<template #title>
<span>
系统管理
{{ menu.name }}
</span>
</template>
<a-menu-item key="setting">
<router-link to="/management/setting">设置</router-link>
</template>
<a-menu-item v-for="submenu in menu.children" :key="submenu.key">
<router-link :to="submenu.path">{{ submenu.name }}</router-link>
</a-menu-item>
</a-sub-menu>
<a-menu-item v-else :key="menu.key">
<router-link :to="menu.path">{{ menu.name }}</router-link>
</a-menu-item>
</a-sub-menu>
</div>
</a-menu>
</a-layout-sider>
<div class="p-3 h-full w-full">

View File

@@ -0,0 +1,121 @@
<script setup>
import {onMounted} from 'vue'
import {amisElideJsonapiAdaptor, amisRender,} from '@/utils.js'
onMounted(() => {
amisRender(
'#amis-organization',
information => {
return {
type: 'page',
body: [
{
type: 'tpl',
tpl: '${baseUrl}1',
},
{
type: 'crud',
syncLocation: false,
headerToolbar: [
'reload',
{
type: 'action',
icon: 'fa fa-plus',
label: '',
actionType: 'dialog',
dialog: {
title: '新增组织',
body: {
type: 'form',
api: {
method: 'post',
url: `${information.baseUrl}/organization/save`,
data: {
code: '${code|default:undefined}',
name: '${name|default:undefined}',
},
},
body: [
{
type: 'input-text',
name: 'code',
label: '组织编号',
placeholder: '不填则自动生成',
},
{
type: 'input-text',
name: 'name',
label: '组织名称',
required: true,
},
],
},
},
},
],
api: {
method: 'get',
url: `${information.baseUrl}/jsonapi/organization`,
data: {
fields: {
organization: 'code,name'
},
page: {
size: '${perPage|default:undefined}',
number: '${page|default:undefined}',
},
},
adaptor: amisElideJsonapiAdaptor,
},
columns: [
{
name: 'id',
hidden: true,
},
{
width: 150,
name: 'code',
label: '组织编号',
},
{
name: 'name',
label: '组织名称',
},
{
width: 100,
label: '操作',
type: 'operation',
buttons: [
{
type: 'action',
icon: 'fa fa-trash',
label: '删除',
level: 'danger',
size: 'xs',
actionType: 'ajax',
api: {
method: 'delete',
url: `${information.baseUrl}/jsonapi/organization/\${id}`,
},
messages: {
success: '删除成功',
failed: '删除失败',
}
}
]
}
],
},
],
}
},
)
})
</script>
<template>
<div id="amis-organization"></div>
</template>
<style scoped>
</style>

View File

@@ -1,6 +1,6 @@
<script setup>
import {onMounted} from 'vue'
import {amisRender} from '../../utils.js'
import {amisRender} from '@/utils.js'
onMounted(() => {
amisRender(

View File

@@ -1,6 +1,6 @@
<script setup>
import {onMounted} from 'vue'
import {amisRender} from '../../utils.js'
import {amisRender} from '@/utils.js'
onMounted(() => {
amisRender(

View File

@@ -1,9 +1,16 @@
import {defineConfig} from 'vite'
import obfuscatorPlugin from "vite-plugin-javascript-obfuscator";
import vue from '@vitejs/plugin-vue'
import {fileURLToPath, URL} from 'node:url'
// https://vite.dev/config/
export default defineConfig({
resolve: {
alias: {
'vue': 'vue/dist/vue.esm-bundler.js',
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
plugins: [
vue(),
obfuscatorPlugin({

61
pom.xml
View File

@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-apijson-server</artifactId>
<artifactId>spring-boot-server-template</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
@@ -16,13 +16,23 @@
<spring-boot.version>3.4.0</spring-boot.version>
<spring-cloud.version>2024.0.0</spring-cloud.version>
<hutool.version>5.8.32</hutool.version>
<elide.version>7.1.4</elide.version>
<mapstruct.version>1.6.3</mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -35,7 +45,12 @@
<dependency>
<groupId>com.yahoo.elide</groupId>
<artifactId>elide-spring-boot-starter</artifactId>
<version>${elide.version}</version>
<version>7.1.4</version>
</dependency>
<dependency>
<groupId>com.blinkfox</groupId>
<artifactId>fenix-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
@@ -43,14 +58,23 @@
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
@@ -76,6 +100,33 @@
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amapstruct.defaultComponentModel=spring</arg>
<arg>-Amapstruct.defaultInjectionStrategy=constructor</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-build</artifactId>

View File

@@ -1,9 +1,13 @@
package com.lanyuanxiaoyao.server;
import com.blinkfox.fenix.EnableFenix;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -12,6 +16,8 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
* @since 2025-03-03
*/
@Slf4j
@EnableFenix
@EnableConfigurationProperties
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
@@ -31,4 +37,10 @@ public class ServerApplication {
}
};
}
@Bean
public ApplicationRunner initialRunner() {
return args -> {
};
}
}

View File

@@ -0,0 +1,9 @@
package com.lanyuanxiaoyao.server.configuration;
/**
* @author lanyuanxiaoyao
* @version 20250327
*/
public interface Constants {
String DATABASE_TABLE_PREFIX = "platform_";
}

View File

@@ -0,0 +1,113 @@
package com.lanyuanxiaoyao.server.configuration.database;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.time.Instant;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.annotations.IdGeneratorType;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 使用雪花算法作为ID生成器
*
* @author lanyuanxiaoyao
* @date 2024-11-14
*/
@Slf4j
public class SnowflakeId {
@IdGeneratorType(IdGenerator.class)
@Retention(RUNTIME)
@Target({FIELD, METHOD})
public @interface Generator {
}
public static final class IdGenerator implements IdentifierGenerator {
@Override
public Object generate(SharedSessionContractImplementor session, Object object) {
try {
return Snowflake.next();
} catch (Exception e) {
log.error("Generate snowflake id failed", e);
throw new RuntimeException(e);
}
}
}
public static long nextId() {
return Snowflake.next();
}
public static String nextStrId() {
return String.valueOf(Snowflake.next());
}
private static class Snowflake {
/**
* 起始的时间戳
*/
private final static long START_TIMESTAMP = 1;
/**
* 序列号占用的位数
*/
private final static long SEQUENCE_BIT = 11;
/**
* 序列号最大值
*/
private final static long MAX_SEQUENCE_BIT = ~(-1 << SEQUENCE_BIT);
/**
* 时间戳值向左位移
*/
private final static long TIMESTAMP_OFFSET = SEQUENCE_BIT;
/**
* 序列号
*/
private static long sequence = 0;
/**
* 上一次时间戳
*/
private static long lastTimestamp = -1;
public static synchronized long next() {
long currentTimestamp = nowTimestamp();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock have moved backwards.");
}
if (currentTimestamp == lastTimestamp) {
// 相同毫秒内, 序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE_BIT;
// 同一毫秒的序列数已经达到最大
if (sequence == 0) {
currentTimestamp = nextTimestamp();
}
} else {
// 不同毫秒内, 序列号置为0
sequence = 0;
}
lastTimestamp = currentTimestamp;
return (currentTimestamp - START_TIMESTAMP) << TIMESTAMP_OFFSET | sequence;
}
private static long nextTimestamp() {
long milli = nowTimestamp();
while (milli <= lastTimestamp) {
milli = nowTimestamp();
}
return milli;
}
private static long nowTimestamp() {
return Instant.now().toEpochMilli();
}
}
}

View File

@@ -0,0 +1,34 @@
package com.lanyuanxiaoyao.server.controller;
import com.lanyuanxiaoyao.server.controller.base.AbstractController;
import com.lanyuanxiaoyao.server.entity.Organization;
import com.lanyuanxiaoyao.server.entity.mapper.EntityMapper;
import com.lanyuanxiaoyao.server.entity.mapper.OrganizationMapper;
import com.lanyuanxiaoyao.server.service.OrganizationService;
import com.lanyuanxiaoyao.server.service.base.AbstractService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("organization")
public class OrganizationController extends AbstractController<Organization, Long, Organization.SaveVO> {
private final OrganizationService organizationService;
private final OrganizationMapper organizationMapper;
public OrganizationController(OrganizationService organizationService, OrganizationMapper organizationMapper) {
this.organizationService = organizationService;
this.organizationMapper = organizationMapper;
}
@Override
public AbstractService<Organization, Long> getService() {
return organizationService;
}
@Override
public EntityMapper<Organization, Organization.SaveVO> getEntityMapper() {
return organizationMapper;
}
}

View File

@@ -0,0 +1,24 @@
package com.lanyuanxiaoyao.server.controller.base;
import com.lanyuanxiaoyao.server.entity.mapper.EntityMapper;
import com.lanyuanxiaoyao.server.service.base.AbstractService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
public abstract class AbstractController<ENTITY, ID, SAVE_VO> {
public abstract AbstractService<ENTITY, ID> getService();
public abstract EntityMapper<ENTITY, SAVE_VO> getEntityMapper();
@GetMapping("/get/{id}")
public ENTITY get(@PathVariable("id") ID id) {
return getService().get(id);
}
@PostMapping("/save")
public ID save(@RequestBody SAVE_VO entity) {
return getService().save(getEntityMapper().fromVO(entity));
}
}

View File

@@ -0,0 +1,26 @@
package com.lanyuanxiaoyao.server.controller.base;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 处理错误信息
*
* @author lanyuanxiaoyao
* @date 2024-01-02
*/
@RestControllerAdvice
public class ErrorController {
private static final Logger logger = LoggerFactory.getLogger(ErrorController.class);
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Throwable.class)
public String errorHandler(Throwable throwable) {
logger.error("Error", throwable);
return throwable.getMessage();
}
}

View File

@@ -0,0 +1,61 @@
package com.lanyuanxiaoyao.server.entity;
import com.lanyuanxiaoyao.server.configuration.Constants;
import com.lanyuanxiaoyao.server.configuration.database.SnowflakeId;
import com.yahoo.elide.annotation.Include;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;
/**
* 组织
*
* @author lanyuanxiaoyao
* @version 20250327
*/
@Getter
@Setter
@ToString
@Entity
@Table(name = Constants.DATABASE_TABLE_PREFIX + "department")
@Include
public class Department {
@Id
@GeneratedValue(generator = "snowflakeId")
@GenericGenerator(name = "snowflakeId", type = SnowflakeId.IdGenerator.class)
private Long id;
private String name;
@ManyToOne(optional = false)
@JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Organization organization;
@ManyToOne
@JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Department parent;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "parent")
@ToString.Exclude
private Set<Department> children;
@Getter
@Setter
@ToString
public static class SaveVO {
private Long id;
private String name;
private Long organizationId;
private Long parentId;
}
}

View File

@@ -1,20 +0,0 @@
package com.lanyuanxiaoyao.server.entity;
import com.yahoo.elide.annotation.LifeCycleHookBinding;
import com.yahoo.elide.core.lifecycle.LifeCycleHook;
import com.yahoo.elide.core.security.ChangeSpec;
import com.yahoo.elide.core.security.RequestScope;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
/**
* @author lanyuanxiaoyao
* @version 20250304
*/
@Slf4j
public class LogicDeletedHook implements LifeCycleHook<User> {
@Override
public void execute(LifeCycleHookBinding.Operation operation, LifeCycleHookBinding.TransactionPhase phase, User user, RequestScope scope, Optional<ChangeSpec> changes) {
log.info("Operation: {}, Phase: {}, User: {}, Scope: {}, Changes: {}", operation, phase, user, scope, changes);
}
}

View File

@@ -0,0 +1,53 @@
package com.lanyuanxiaoyao.server.entity;
import com.lanyuanxiaoyao.server.configuration.Constants;
import com.lanyuanxiaoyao.server.configuration.database.SnowflakeId;
import com.yahoo.elide.annotation.Include;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;
/**
* 组织
*
* @author lanyuanxiaoyao
* @version 20250327
*/
@Getter
@Setter
@ToString
@Entity
@Table(name = Constants.DATABASE_TABLE_PREFIX + "organization")
@Include
public class Organization {
@Id
@GeneratedValue(generator = "snowflakeId")
@GenericGenerator(name = "snowflakeId", type = SnowflakeId.IdGenerator.class)
private Long id;
@Column(unique = true, nullable = false)
private String code;
@Column(nullable = false)
private String name;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "organization")
@ToString.Exclude
private Set<Department> departments;
@Getter
@Setter
@ToString
public static final class SaveVO {
private Long id;
private String code;
private String name;
}
}

View File

@@ -1,36 +0,0 @@
package com.lanyuanxiaoyao.server.entity;
import com.yahoo.elide.annotation.Include;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.DynamicUpdate;
/**
* @author lanyuanxiaoyao
* @version 20250304
*/
@Getter
@Setter
@ToString
@Entity
@DynamicUpdate
@Include
public class School {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(nullable = false)
private String name;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "school")
@ToString.Exclude
private List<User> users;
}

View File

@@ -1,43 +0,0 @@
package com.lanyuanxiaoyao.server.entity;
import com.yahoo.elide.annotation.Include;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SoftDelete;
/**
* @author lanyuanxiaoyao
* @version 20250304
*/
@Getter
@Setter
@ToString
@Entity
@DynamicUpdate
@Include
@SoftDelete
public class User {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@ToString.Exclude
private School school;
}

View File

@@ -0,0 +1,26 @@
package com.lanyuanxiaoyao.server.entity.mapper;
import com.lanyuanxiaoyao.server.entity.Department;
import com.lanyuanxiaoyao.server.service.DepartmentService;
import com.lanyuanxiaoyao.server.service.OrganizationService;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* 部门bean转换
*
* @author lanyuanxiaoyao
* @version 20250327
*/
@Mapper(
uses = {
OrganizationService.class,
DepartmentService.class,
}
)
public interface DepartmentMapper extends EntityMapper<Department, Department.SaveVO> {
@Mapping(target = "organization", source = "organizationId")
@Mapping(target = "parent", source = "parentId")
@Override
Department fromVO(Department.SaveVO saveVO);
}

View File

@@ -0,0 +1,5 @@
package com.lanyuanxiaoyao.server.entity.mapper;
public interface EntityMapper<SE, CE> {
SE fromVO(CE creationVO);
}

View File

@@ -0,0 +1,23 @@
package com.lanyuanxiaoyao.server.entity.mapper;
import com.lanyuanxiaoyao.server.configuration.database.SnowflakeId;
import com.lanyuanxiaoyao.server.entity.Organization;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* 组织bean转换
*
* @author lanyuanxiaoyao
* @version 20250327
*/
@Mapper(
imports = {
SnowflakeId.class
}
)
public interface OrganizationMapper extends EntityMapper<Organization, Organization.SaveVO> {
@Mapping(target = "code", defaultExpression = "java(SnowflakeId.nextStrId())")
@Override
Organization fromVO(Organization.SaveVO creationVO);
}

View File

@@ -0,0 +1,10 @@
package com.lanyuanxiaoyao.server.repository;
import com.blinkfox.fenix.jpa.FenixJpaRepository;
import com.blinkfox.fenix.specification.FenixJpaSpecificationExecutor;
import com.lanyuanxiaoyao.server.entity.Department;
import org.springframework.stereotype.Repository;
@Repository
public interface DepartmentRepository extends FenixJpaRepository<Department, Long>, FenixJpaSpecificationExecutor<Department> {
}

View File

@@ -0,0 +1,12 @@
package com.lanyuanxiaoyao.server.repository;
import com.blinkfox.fenix.jpa.FenixJpaRepository;
import com.blinkfox.fenix.specification.FenixJpaSpecificationExecutor;
import com.lanyuanxiaoyao.server.entity.Organization;
import org.springframework.stereotype.Repository;
@Repository
public interface OrganizationRepository extends FenixJpaRepository<Organization, Long>, FenixJpaSpecificationExecutor<Organization> {
@Override
Organization getReferenceById(Long aLong);
}

View File

@@ -0,0 +1,28 @@
package com.lanyuanxiaoyao.server.service;
import com.blinkfox.fenix.jpa.FenixJpaRepository;
import com.lanyuanxiaoyao.server.entity.Department;
import com.lanyuanxiaoyao.server.repository.DepartmentRepository;
import com.lanyuanxiaoyao.server.service.base.AbstractService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class DepartmentService extends AbstractService<Department, Long> {
private final DepartmentRepository departmentRepository;
public DepartmentService(DepartmentRepository departmentRepository) {
this.departmentRepository = departmentRepository;
}
@Override
public FenixJpaRepository<Department, Long> getRepository() {
return departmentRepository;
}
@Override
public Long getId(Department department) {
return department.getId();
}
}

View File

@@ -0,0 +1,28 @@
package com.lanyuanxiaoyao.server.service;
import com.blinkfox.fenix.jpa.FenixJpaRepository;
import com.lanyuanxiaoyao.server.entity.Organization;
import com.lanyuanxiaoyao.server.repository.OrganizationRepository;
import com.lanyuanxiaoyao.server.service.base.AbstractService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OrganizationService extends AbstractService<Organization, Long> {
private final OrganizationRepository organizationRepository;
public OrganizationService(OrganizationRepository organizationRepository) {
this.organizationRepository = organizationRepository;
}
@Override
public FenixJpaRepository<Organization, Long> getRepository() {
return organizationRepository;
}
@Override
public Long getId(Organization organization) {
return organization.getId();
}
}

View File

@@ -0,0 +1,20 @@
package com.lanyuanxiaoyao.server.service.base;
import com.blinkfox.fenix.jpa.FenixJpaRepository;
import org.springframework.transaction.annotation.Transactional;
public abstract class AbstractService<ENTITY, ID> {
public abstract FenixJpaRepository<ENTITY, ID> getRepository();
public abstract ID getId(ENTITY entity);
public ENTITY get(ID id) {
return getRepository().getReferenceById(id);
}
@Transactional(rollbackFor = Exception.class)
public ID save(ENTITY entity) {
ENTITY saved = getRepository().saveOrUpdateByNotNullProperties(entity);
return getId(saved);
}
}

View File

@@ -1,12 +1,18 @@
spring:
datasource:
url: jdbc:mysql://localhost:3307/main?useSSL=false&allowPublicKeyRetrieval=true
url: jdbc:h2:file:./database;DB_CLOSE_ON_EXIT=FALSE
username: test
password: test
driver-class-name: com.mysql.cj.jdbc.Driver
driver-class-name: org.h2.Driver
jpa:
show-sql: true
generate-ddl: true
servlet:
multipart:
max-file-size: 10MB
max-request-size: 20MB
jackson:
date-format: 'yyyy-MM-dd HH:mm:ss'
elide:
json-api:
enabled: true
@@ -22,4 +28,6 @@ logging:
datastores:
jpql:
query:
DefaultQueryLogger: debug
DefaultQueryLogger: debug
fenix:
print-banner: false

View File

@@ -0,0 +1,20 @@
### Create
POST http://localhost:8080/jsonapi/organization
Content-Type: application/vnd.api+json
{
"data": {
"type": "organization",
"attributes": {
"organizationName": "苹果公司"
}
}
}
### Create
POST http://localhost:8080/graphql
Content-Type: application/json
{
"query": "mutation {organization(op: UPSERT, data: {organizationName: \"苹果\"}) {edges {node {organizationId organizationName}}}}"
}