Expand EasyCode front workbench features
This commit is contained in:
@@ -1 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
VITE_GENERATE_TIMEOUT=180000
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
VITE_API_BASE_URL=/front
|
||||
VITE_GENERATE_TIMEOUT=180000
|
||||
|
||||
1177
RuoYi-Vue/easycode-web/package-lock.json
generated
1177
RuoYi-Vue/easycode-web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import request from './request'
|
||||
import { GENERATE_REQUEST_TIMEOUT } from './timeouts'
|
||||
|
||||
function unwrap(res) {
|
||||
return res && Object.prototype.hasOwnProperty.call(res, 'data') ? res.data : res
|
||||
@@ -12,10 +13,32 @@ export function createProject(data) {
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function listProjects() {
|
||||
return request({
|
||||
url: '/front/project/list',
|
||||
method: 'get'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function getProject(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}`,
|
||||
method: 'get'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function deleteProject(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}`,
|
||||
method: 'delete'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function generateDatabase(projectId, data) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/generate-database`,
|
||||
method: 'post',
|
||||
timeout: GENERATE_REQUEST_TIMEOUT,
|
||||
data
|
||||
}).then(unwrap)
|
||||
}
|
||||
@@ -38,7 +61,8 @@ export function saveDatabase(projectId, data) {
|
||||
export function generateProject(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/preview`,
|
||||
method: 'post'
|
||||
method: 'post',
|
||||
timeout: GENERATE_REQUEST_TIMEOUT
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
@@ -57,3 +81,13 @@ export function getFileContent(projectId, params) {
|
||||
params
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function downloadProject(projectId, templateType) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/download`,
|
||||
method: 'get',
|
||||
params: templateType ? { templateType } : undefined,
|
||||
responseType: 'blob',
|
||||
timeout: GENERATE_REQUEST_TIMEOUT
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getToken, removeToken } from '@/utils/auth'
|
||||
import { DEFAULT_REQUEST_TIMEOUT } from './timeouts'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: '',
|
||||
timeout: 30000
|
||||
timeout: DEFAULT_REQUEST_TIMEOUT
|
||||
})
|
||||
|
||||
service.interceptors.request.use((config) => {
|
||||
|
||||
45
RuoYi-Vue/easycode-web/src/api/source.js
Normal file
45
RuoYi-Vue/easycode-web/src/api/source.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import request from './request'
|
||||
import { normalizeSourceCategories, normalizeSourceProject, normalizeSourceProjects } from './sourceAdapter'
|
||||
|
||||
export function listSourceCategories() {
|
||||
return request({
|
||||
url: '/front/source/categories',
|
||||
method: 'get'
|
||||
}).then((result) => normalizeSourceCategories(result.data))
|
||||
}
|
||||
|
||||
export function listSourceProjects(params = {}) {
|
||||
return request({
|
||||
url: '/front/source/projects',
|
||||
method: 'get',
|
||||
params
|
||||
}).then((result) => normalizeSourceProjects(result.data))
|
||||
}
|
||||
|
||||
export function getSourceProject(slug) {
|
||||
return request({
|
||||
url: `/front/source/projects/${slug}`,
|
||||
method: 'get'
|
||||
}).then((result) => normalizeSourceProject(result.data))
|
||||
}
|
||||
|
||||
export function purchaseSourceProject(projectId) {
|
||||
return request({
|
||||
url: `/front/source/projects/${projectId}/purchase`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function listSourcePurchases() {
|
||||
return request({
|
||||
url: '/front/source/purchases',
|
||||
method: 'get'
|
||||
}).then((result) => result.data || [])
|
||||
}
|
||||
|
||||
export function getSourceDownloadInfo(projectId) {
|
||||
return request({
|
||||
url: `/front/source/projects/${projectId}/download`,
|
||||
method: 'get'
|
||||
}).then((result) => result.data)
|
||||
}
|
||||
82
RuoYi-Vue/easycode-web/src/api/sourceAdapter.js
Normal file
82
RuoYi-Vue/easycode-web/src/api/sourceAdapter.js
Normal file
@@ -0,0 +1,82 @@
|
||||
export function normalizeSourceProject(project = {}) {
|
||||
return {
|
||||
projectId: project.projectId,
|
||||
id: project.slug || String(project.projectId || ''),
|
||||
name: project.projectName || project.name || '',
|
||||
category: project.category || '',
|
||||
coverText: project.coverText || '',
|
||||
summary: project.summary || '',
|
||||
descriptionMd: project.descriptionMd || '',
|
||||
price: project.priceLabel || formatPrice(project.price),
|
||||
priceValue: Number(project.price || 0),
|
||||
views: Number(project.viewCount || project.views || 0),
|
||||
updateTime: normalizeDate(project.updateTime),
|
||||
tags: normalizeList(project.tags),
|
||||
modules: normalizeList(project.modules),
|
||||
scenes: normalizeList(project.scenes),
|
||||
purchased: Boolean(project.purchased),
|
||||
resourceReady: Boolean(project.resourceReady)
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSourceProjects(projects = []) {
|
||||
return Array.isArray(projects) ? projects.map(normalizeSourceProject) : []
|
||||
}
|
||||
|
||||
export function normalizeSourceCategories(categories = []) {
|
||||
if (!Array.isArray(categories)) return []
|
||||
return categories
|
||||
.map((category) => ({
|
||||
value: category.value || category.dictValue || '',
|
||||
label: category.label || category.dictLabel || category.value || category.dictValue || ''
|
||||
}))
|
||||
.filter((category) => category.value)
|
||||
}
|
||||
|
||||
export function buildSourceCategories(projects = [], sourceCategories = []) {
|
||||
const seen = new Set()
|
||||
const categories = [{ value: 'all', label: '全部源码' }]
|
||||
normalizeSourceCategories(sourceCategories).forEach((category) => {
|
||||
if (seen.has(category.value)) return
|
||||
seen.add(category.value)
|
||||
categories.push(category)
|
||||
})
|
||||
projects.forEach((project) => {
|
||||
const value = project.category
|
||||
if (!value || seen.has(value)) return
|
||||
seen.add(value)
|
||||
categories.push({
|
||||
value,
|
||||
label: value
|
||||
})
|
||||
})
|
||||
return categories
|
||||
}
|
||||
|
||||
function normalizeList(value) {
|
||||
if (Array.isArray(value)) return value
|
||||
if (!value) return []
|
||||
if (typeof value !== 'string') return []
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
return Array.isArray(parsed) ? parsed : []
|
||||
} catch (error) {
|
||||
return value
|
||||
.split(/[,,\n]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDate(value) {
|
||||
if (!value) return ''
|
||||
if (typeof value === 'string') return value.slice(0, 10)
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function formatPrice(cents) {
|
||||
const value = Number(cents || 0) / 100
|
||||
return `¥${Number.isInteger(value) ? value : value.toFixed(2)}`
|
||||
}
|
||||
46
RuoYi-Vue/easycode-web/src/api/sourceAdapter.test.mjs
Normal file
46
RuoYi-Vue/easycode-web/src/api/sourceAdapter.test.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildSourceCategories, normalizeSourceProject } from './sourceAdapter.js'
|
||||
|
||||
const backendProject = {
|
||||
projectId: 10,
|
||||
slug: 'dms-platform',
|
||||
projectName: 'Commercial DMS',
|
||||
category: 'enterprise',
|
||||
coverText: 'DMS',
|
||||
summary: 'DMS source',
|
||||
descriptionMd: '# DMS',
|
||||
price: 39900,
|
||||
priceLabel: '¥399',
|
||||
viewCount: 2860,
|
||||
updateTime: '2026-05-18 12:00:00',
|
||||
tags: ['Spring Boot', 'Vue2'],
|
||||
modules: ['Dealer', 'Repair'],
|
||||
scenes: ['Aftermarket'],
|
||||
purchased: true
|
||||
}
|
||||
|
||||
const project = normalizeSourceProject(backendProject)
|
||||
|
||||
assert.equal(project.id, 'dms-platform')
|
||||
assert.equal(project.projectId, 10)
|
||||
assert.equal(project.name, 'Commercial DMS')
|
||||
assert.equal(project.price, '¥399')
|
||||
assert.equal(project.views, 2860)
|
||||
assert.equal(project.updateTime, '2026-05-18')
|
||||
assert.equal(project.tags[1], 'Vue2')
|
||||
assert.equal(project.modules[0], 'Dealer')
|
||||
assert.equal(project.purchased, true)
|
||||
|
||||
const categories = buildSourceCategories(
|
||||
[
|
||||
backendProject,
|
||||
{ ...backendProject, projectId: 11, slug: 'mall', category: 'mall' }
|
||||
],
|
||||
[
|
||||
{ dictValue: 'enterprise', dictLabel: 'Enterprise Systems' },
|
||||
{ dictValue: 'mall', dictLabel: 'Mall Systems' }
|
||||
]
|
||||
)
|
||||
|
||||
assert.deepEqual(categories.map((category) => category.value), ['all', 'enterprise', 'mall'])
|
||||
assert.deepEqual(categories.map((category) => category.label), ['全部源码', 'Enterprise Systems', 'Mall Systems'])
|
||||
9
RuoYi-Vue/easycode-web/src/api/timeouts.js
Normal file
9
RuoYi-Vue/easycode-web/src/api/timeouts.js
Normal file
@@ -0,0 +1,9 @@
|
||||
function toPositiveNumber(value, fallback) {
|
||||
const number = Number(value)
|
||||
return Number.isFinite(number) && number > 0 ? number : fallback
|
||||
}
|
||||
|
||||
const env = import.meta.env || {}
|
||||
|
||||
export const DEFAULT_REQUEST_TIMEOUT = toPositiveNumber(env.VITE_API_TIMEOUT, 30000)
|
||||
export const GENERATE_REQUEST_TIMEOUT = toPositiveNumber(env.VITE_GENERATE_TIMEOUT, 180000)
|
||||
@@ -8,6 +8,8 @@
|
||||
<nav class="nav">
|
||||
<router-link to="/">工作台</router-link>
|
||||
<router-link to="/generate">生成项目</router-link>
|
||||
<router-link to="/source-store">源码库</router-link>
|
||||
<router-link to="/projects">我的项目</router-link>
|
||||
</nav>
|
||||
|
||||
<div class="actions">
|
||||
|
||||
@@ -323,7 +323,8 @@ function removeField(index) {
|
||||
|
||||
.delete-col {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
align-items: flex-start;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.field-head {
|
||||
@@ -347,5 +348,9 @@ function removeField(index) {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #e3e8f0;
|
||||
}
|
||||
|
||||
.delete-col {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
514
RuoYi-Vue/easycode-web/src/components/ErDiagramView.vue
Normal file
514
RuoYi-Vue/easycode-web/src/components/ErDiagramView.vue
Normal file
@@ -0,0 +1,514 @@
|
||||
<template>
|
||||
<div class="er-diagram">
|
||||
<el-empty v-if="!graph.nodes.length" description="生成数据库后可查看 ER 图" />
|
||||
<template v-else>
|
||||
<div class="er-toolbar">
|
||||
<div class="relation-builder">
|
||||
<el-select v-model="relationForm.source" placeholder="来源表">
|
||||
<el-option v-for="node in graph.nodes" :key="node.id" :label="node.title" :value="node.id" />
|
||||
</el-select>
|
||||
<el-select v-model="relationForm.sourceField" placeholder="来源字段">
|
||||
<el-option v-for="field in sourceFields" :key="field.id" :label="field.name" :value="field.name" />
|
||||
</el-select>
|
||||
<span class="relation-arrow">→</span>
|
||||
<el-select v-model="relationForm.target" placeholder="目标表">
|
||||
<el-option v-for="node in graph.nodes" :key="node.id" :label="node.title" :value="node.id" />
|
||||
</el-select>
|
||||
<el-select v-model="relationForm.targetField" placeholder="目标字段">
|
||||
<el-option v-for="field in targetFields" :key="field.id" :label="field.name" :value="field.name" />
|
||||
</el-select>
|
||||
<el-button type="primary" :icon="Plus" @click="addRelation">添加关系</el-button>
|
||||
</div>
|
||||
<el-button :icon="Picture" @click="exportPng">导出图片</el-button>
|
||||
</div>
|
||||
|
||||
<div class="er-scroll">
|
||||
<svg ref="svgRef" class="er-canvas" :viewBox="`0 0 ${canvas.width} ${canvas.height}`" :style="canvasStyle">
|
||||
<defs>
|
||||
<marker
|
||||
id="er-arrow"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="8"
|
||||
refY="3"
|
||||
orient="auto"
|
||||
markerUnits="strokeWidth"
|
||||
>
|
||||
<path d="M0,0 L0,6 L8,3 z" fill="#8aa0bc" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<g class="edge-layer">
|
||||
<g v-for="edge in edgeViews" :key="edge.id">
|
||||
<path class="relation-line" :class="{ manual: !edge.inferred }" :d="edge.path" marker-end="url(#er-arrow)" />
|
||||
<text class="relation-label" :x="edge.labelX" :y="edge.labelY">{{ edge.fieldName }}</text>
|
||||
<g class="edge-delete" :transform="`translate(${edge.labelX + 44}, ${edge.labelY - 13})`" @click="removeRelation(edge)">
|
||||
<circle r="9" />
|
||||
<path d="M-4,-4 L4,4 M4,-4 L-4,4" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g class="node-layer">
|
||||
<foreignObject
|
||||
v-for="node in graph.nodes"
|
||||
:key="node.id"
|
||||
:x="node.x"
|
||||
:y="node.y"
|
||||
:width="node.width"
|
||||
:height="node.height"
|
||||
>
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" class="entity-node" @pointerdown="startDrag($event, node)">
|
||||
<div class="entity-head">
|
||||
<strong>{{ node.title }}</strong>
|
||||
<span v-if="node.comment">{{ node.comment }}</span>
|
||||
</div>
|
||||
<div class="field-list">
|
||||
<div v-for="field in node.fields" :key="field.id" class="field-row">
|
||||
<span v-if="field.primary" class="field-badge primary">PK</span>
|
||||
<span v-else-if="field.required" class="field-badge required">*</span>
|
||||
<span v-else class="field-badge muted">-</span>
|
||||
<span class="field-name">{{ field.name }}</span>
|
||||
<small v-if="field.comment">{{ field.comment }}</small>
|
||||
</div>
|
||||
<div v-if="!node.fields.length" class="field-empty">暂无字段</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Picture, Plus } from '@element-plus/icons-vue'
|
||||
import { buildErExportSvg } from '@/utils/erExport'
|
||||
import { buildErGraph } from '@/utils/erGraph'
|
||||
import { saveBlob } from '@/utils/download'
|
||||
|
||||
const FIELD_HEIGHT = 28
|
||||
const HEADER_HEIGHT = 60
|
||||
|
||||
const props = defineProps({
|
||||
tables: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
draft: {
|
||||
type: Object,
|
||||
default: () => ({ positions: {}, relations: [], deletedEdgeIds: [] })
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:draft'])
|
||||
const svgRef = ref(null)
|
||||
const relationForm = reactive({
|
||||
source: '',
|
||||
sourceField: '',
|
||||
target: '',
|
||||
targetField: ''
|
||||
})
|
||||
const dragging = ref(null)
|
||||
|
||||
const draftValue = computed(() => normalizeDraft(props.draft))
|
||||
const graph = computed(() => buildErGraph(props.tables, draftValue.value))
|
||||
|
||||
const nodeMap = computed(() => {
|
||||
const map = new Map()
|
||||
graph.value.nodes.forEach((node) => map.set(node.id, node))
|
||||
return map
|
||||
})
|
||||
|
||||
const canvas = computed(() => {
|
||||
const nodes = graph.value.nodes
|
||||
const width = Math.max(900, ...nodes.map((node) => node.x + node.width + 48))
|
||||
const height = Math.max(480, ...nodes.map((node) => node.y + node.height + 48))
|
||||
return { width, height }
|
||||
})
|
||||
|
||||
const canvasStyle = computed(() => ({
|
||||
width: `${canvas.value.width}px`,
|
||||
height: `${canvas.value.height}px`
|
||||
}))
|
||||
|
||||
const sourceFields = computed(() => fieldsOf(relationForm.source))
|
||||
const targetFields = computed(() => fieldsOf(relationForm.target))
|
||||
|
||||
watch(
|
||||
() => graph.value.nodes.map((node) => node.id).join(','),
|
||||
() => ensureRelationDefaults(),
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => relationForm.source,
|
||||
() => {
|
||||
relationForm.sourceField = sourceFields.value[0]?.name || ''
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => relationForm.target,
|
||||
() => {
|
||||
relationForm.targetField = targetFields.value[0]?.name || ''
|
||||
}
|
||||
)
|
||||
|
||||
function normalizeDraft(value) {
|
||||
const source = value && typeof value === 'object' ? value : {}
|
||||
return {
|
||||
positions: source.positions && typeof source.positions === 'object' ? source.positions : {},
|
||||
relations: Array.isArray(source.relations) ? source.relations : [],
|
||||
deletedEdgeIds: Array.isArray(source.deletedEdgeIds) ? source.deletedEdgeIds : []
|
||||
}
|
||||
}
|
||||
|
||||
function emitDraft(patch) {
|
||||
emit('update:draft', {
|
||||
...draftValue.value,
|
||||
...patch
|
||||
})
|
||||
}
|
||||
|
||||
function fieldsOf(nodeId) {
|
||||
return nodeMap.value.get(nodeId)?.fields || []
|
||||
}
|
||||
|
||||
function ensureRelationDefaults() {
|
||||
const nodes = graph.value.nodes
|
||||
if (!relationForm.source && nodes[0]) {
|
||||
relationForm.source = nodes[0].id
|
||||
}
|
||||
if (!relationForm.target && nodes[1]) {
|
||||
relationForm.target = nodes[1].id
|
||||
}
|
||||
if (!relationForm.target && nodes[0]) {
|
||||
relationForm.target = nodes[0].id
|
||||
}
|
||||
if (!relationForm.sourceField) {
|
||||
relationForm.sourceField = sourceFields.value[0]?.name || ''
|
||||
}
|
||||
if (!relationForm.targetField) {
|
||||
relationForm.targetField = targetFields.value[0]?.name || ''
|
||||
}
|
||||
}
|
||||
|
||||
function fieldY(node, fieldName) {
|
||||
const index = Math.max(0, node.fields.findIndex((field) => field.name === fieldName))
|
||||
return node.y + HEADER_HEIGHT + index * FIELD_HEIGHT + FIELD_HEIGHT / 2
|
||||
}
|
||||
|
||||
function edgeAnchors(edge) {
|
||||
const source = nodeMap.value.get(edge.source)
|
||||
const target = nodeMap.value.get(edge.target)
|
||||
if (!source || !target) return null
|
||||
|
||||
const sourceOnLeft = source.x <= target.x
|
||||
const sourceX = sourceOnLeft ? source.x + source.width : source.x
|
||||
const targetX = sourceOnLeft ? target.x : target.x + target.width
|
||||
return {
|
||||
sourceX,
|
||||
sourceY: fieldY(source, edge.fieldName),
|
||||
targetX,
|
||||
targetY: fieldY(target, edge.targetFieldName),
|
||||
curve: Math.max(56, Math.abs(targetX - sourceX) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
function addRelation() {
|
||||
if (!relationForm.source || !relationForm.target || !relationForm.sourceField || !relationForm.targetField) {
|
||||
ElMessage.warning('请选择关系两端的表和字段')
|
||||
return
|
||||
}
|
||||
if (relationForm.source === relationForm.target) {
|
||||
ElMessage.warning('关系两端不能是同一张表')
|
||||
return
|
||||
}
|
||||
|
||||
const relation = {
|
||||
id: `manual_${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
||||
source: relationForm.source,
|
||||
target: relationForm.target,
|
||||
fieldName: relationForm.sourceField,
|
||||
targetFieldName: relationForm.targetField
|
||||
}
|
||||
emitDraft({ relations: [...draftValue.value.relations, relation] })
|
||||
}
|
||||
|
||||
function removeRelation(edge) {
|
||||
if (edge.inferred) {
|
||||
emitDraft({ deletedEdgeIds: Array.from(new Set([...draftValue.value.deletedEdgeIds, edge.id])) })
|
||||
return
|
||||
}
|
||||
emitDraft({ relations: draftValue.value.relations.filter((relation) => relation.id !== edge.id) })
|
||||
}
|
||||
|
||||
function startDrag(event, node) {
|
||||
dragging.value = {
|
||||
id: node.id,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
x: node.x,
|
||||
y: node.y
|
||||
}
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId)
|
||||
window.addEventListener('pointermove', handleDrag)
|
||||
window.addEventListener('pointerup', stopDrag, { once: true })
|
||||
}
|
||||
|
||||
function handleDrag(event) {
|
||||
if (!dragging.value) return
|
||||
|
||||
const nextX = Math.max(0, Math.round(dragging.value.x + event.clientX - dragging.value.startX))
|
||||
const nextY = Math.max(0, Math.round(dragging.value.y + event.clientY - dragging.value.startY))
|
||||
emitDraft({
|
||||
positions: {
|
||||
...draftValue.value.positions,
|
||||
[dragging.value.id]: { x: nextX, y: nextY }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
dragging.value = null
|
||||
window.removeEventListener('pointermove', handleDrag)
|
||||
}
|
||||
|
||||
async function exportPng() {
|
||||
const svgText = buildErExportSvg({
|
||||
width: canvas.value.width,
|
||||
height: canvas.value.height,
|
||||
nodes: graph.value.nodes,
|
||||
edges: edgeViews.value
|
||||
})
|
||||
const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' })
|
||||
const url = URL.createObjectURL(svgBlob)
|
||||
const image = new Image()
|
||||
image.decoding = 'async'
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
image.onload = resolve
|
||||
image.onerror = reject
|
||||
image.src = url
|
||||
})
|
||||
const canvasElement = document.createElement('canvas')
|
||||
canvasElement.width = canvas.value.width
|
||||
canvasElement.height = canvas.value.height
|
||||
const context = canvasElement.getContext('2d')
|
||||
context.fillStyle = '#f8fafc'
|
||||
context.fillRect(0, 0, canvasElement.width, canvasElement.height)
|
||||
context.drawImage(image, 0, 0)
|
||||
const blob = await new Promise((resolve) => canvasElement.toBlob(resolve, 'image/png'))
|
||||
if (blob) {
|
||||
saveBlob(blob, 'er-diagram.png')
|
||||
}
|
||||
} finally {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
const edgeViews = computed(() => graph.value.edges
|
||||
.map((edge) => {
|
||||
const anchors = edgeAnchors(edge)
|
||||
if (!anchors) return null
|
||||
|
||||
const direction = anchors.sourceX <= anchors.targetX ? 1 : -1
|
||||
const c1x = anchors.sourceX + anchors.curve * direction
|
||||
const c2x = anchors.targetX - anchors.curve * direction
|
||||
return {
|
||||
...edge,
|
||||
path: `M ${anchors.sourceX} ${anchors.sourceY} C ${c1x} ${anchors.sourceY}, ${c2x} ${anchors.targetY}, ${anchors.targetX} ${anchors.targetY}`,
|
||||
labelX: (anchors.sourceX + anchors.targetX) / 2,
|
||||
labelY: (anchors.sourceY + anchors.targetY) / 2 - 8
|
||||
}
|
||||
})
|
||||
.filter(Boolean))
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.er-diagram {
|
||||
min-height: 430px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.er-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid #e3e8f0;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.relation-builder {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 170px) minmax(120px, 170px) auto minmax(120px, 170px) minmax(120px, 170px) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.relation-arrow {
|
||||
color: #64748b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.er-scroll {
|
||||
overflow: auto;
|
||||
min-height: 430px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.er-canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.relation-line {
|
||||
fill: none;
|
||||
stroke: #8aa0bc;
|
||||
stroke-width: 1.6;
|
||||
stroke-dasharray: 6 5;
|
||||
}
|
||||
|
||||
.relation-line.manual {
|
||||
stroke: #2563eb;
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
|
||||
.edge-delete {
|
||||
cursor: pointer;
|
||||
|
||||
circle {
|
||||
fill: #ffffff;
|
||||
stroke: #e2e8f0;
|
||||
}
|
||||
|
||||
path {
|
||||
stroke: #ef4444;
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
}
|
||||
|
||||
.relation-label {
|
||||
fill: #64748b;
|
||||
font-size: 12px;
|
||||
paint-order: stroke;
|
||||
stroke: #f8fafc;
|
||||
stroke-width: 4px;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.entity-node {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border: 1px solid #d6e0ee;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.entity-head {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #e7edf5;
|
||||
background: #eef5ff;
|
||||
|
||||
strong {
|
||||
overflow: hidden;
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.field-list {
|
||||
padding: 8px 10px 10px;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr) minmax(0, 0.9fr);
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 28px;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
|
||||
small {
|
||||
overflow: hidden;
|
||||
color: #94a3b8;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.field-badge {
|
||||
display: inline-grid;
|
||||
min-width: 26px;
|
||||
height: 18px;
|
||||
place-items: center;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-badge.primary {
|
||||
color: #1d4ed8;
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.field-badge.required {
|
||||
color: #b45309;
|
||||
background: #fef3c7;
|
||||
}
|
||||
|
||||
.field-badge.muted {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.field-empty {
|
||||
padding: 18px 0;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.er-toolbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.relation-builder {
|
||||
grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.relation-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
217
RuoYi-Vue/easycode-web/src/data/sourceProjects.js
Normal file
217
RuoYi-Vue/easycode-web/src/data/sourceProjects.js
Normal file
@@ -0,0 +1,217 @@
|
||||
export const sourceCategories = [
|
||||
{ value: 'enterprise', label: '企业管理系统' },
|
||||
{ value: 'mall', label: '商城系统' },
|
||||
{ value: 'office', label: 'OA 办公' },
|
||||
{ value: 'lowcode', label: '低代码平台' },
|
||||
{ value: 'microservice', label: '微服务脚手架' }
|
||||
]
|
||||
|
||||
export const sourceProjects = [
|
||||
{
|
||||
id: 'dms-platform',
|
||||
name: '商用车 DMS 管理系统',
|
||||
category: 'enterprise',
|
||||
coverText: 'DMS',
|
||||
summary: '覆盖经销商、维修工单、索赔、配件库存、服务活动和结算的商用车售后管理源码。',
|
||||
price: '¥399',
|
||||
views: 2860,
|
||||
updateTime: '2026-05-18',
|
||||
tags: ['Spring Boot', 'Vue2', 'MySQL'],
|
||||
modules: ['经销商管理', '维修工单', '配件库存', '索赔结算', '服务活动'],
|
||||
scenes: ['汽车后市场', '商用车售后', '经销商协同'],
|
||||
descriptionMd: `# 商用车 DMS 管理系统
|
||||
|
||||
这套源码面向商用车售后服务场景,提供经销商、车辆档案、维修工单、索赔流程、配件库存和结算对账等核心能力。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- 经销商档案、服务站和维修技师管理
|
||||
- 车辆档案、保养提醒和维修历史追踪
|
||||
- 维修工单、派工、完工质检和费用结算
|
||||
- 配件入库、出库、调拨和库存预警
|
||||
- 索赔申请、审核、结算和统计报表
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
| --- | --- |
|
||||
| 后端 | Spring Boot、MyBatis、Spring Security |
|
||||
| 前端 | Vue2、Element UI |
|
||||
| 数据库 | MySQL |
|
||||
|
||||
## 目录结构
|
||||
|
||||
\`\`\`text
|
||||
dms-platform
|
||||
├─ backend
|
||||
├─ admin-web
|
||||
├─ sql
|
||||
└─ docs
|
||||
\`\`\`
|
||||
|
||||
## 适合人群
|
||||
|
||||
适合需要快速交付售后服务、维修工单、配件库存类系统的开发团队。`
|
||||
},
|
||||
{
|
||||
id: 'mall-order-system',
|
||||
name: '商城订单管理系统',
|
||||
category: 'mall',
|
||||
coverText: 'MALL',
|
||||
summary: '包含商品、订单、支付、库存、优惠券、会员和后台统计模块的商城管理源码。',
|
||||
price: '¥299',
|
||||
views: 1980,
|
||||
updateTime: '2026-05-15',
|
||||
tags: ['Spring Cloud', 'Redis', 'Vue3'],
|
||||
modules: ['商品中心', '订单中心', '支付记录', '会员体系', '营销活动'],
|
||||
scenes: ['电商后台', '订单运营', '会员营销'],
|
||||
descriptionMd: `# 商城订单管理系统
|
||||
|
||||
项目提供从商品上架到订单履约的完整后台管理能力,适合作为中小型商城或订单中台的启动源码。
|
||||
|
||||
## 功能模块
|
||||
|
||||
- 商品分类、SKU、库存和上下架
|
||||
- 购物车、订单、支付记录和售后单
|
||||
- 优惠券、满减活动和会员等级
|
||||
- 后台统计看板和经营数据导出
|
||||
|
||||
## 部署说明
|
||||
|
||||
\`\`\`bash
|
||||
npm install
|
||||
npm run build
|
||||
\`\`\`
|
||||
|
||||
后端服务默认读取 MySQL 与 Redis 配置,可按环境拆分配置文件。`
|
||||
},
|
||||
{
|
||||
id: 'oa-approval-suite',
|
||||
name: 'OA 办公审批系统',
|
||||
category: 'office',
|
||||
coverText: 'OA',
|
||||
summary: '集成用户、角色、菜单、审批流、请假、报销、公告和文件管理的办公源码。',
|
||||
price: '¥268',
|
||||
views: 1750,
|
||||
updateTime: '2026-05-12',
|
||||
tags: ['Sa-Token', 'Element UI', '工作流'],
|
||||
modules: ['审批流程', '请假报销', '公告通知', '文件管理', '权限菜单'],
|
||||
scenes: ['企业内网', '行政审批', '协同办公'],
|
||||
descriptionMd: `# OA 办公审批系统
|
||||
|
||||
这是一套偏企业内网办公的基础源码,重点覆盖审批、公告、文件与权限管理。
|
||||
|
||||
## 亮点
|
||||
|
||||
- 可配置审批节点和审批人
|
||||
- 请假、报销、外出等常见办公流程
|
||||
- 公告通知、附件上传和文件归档
|
||||
- 角色、菜单、按钮级权限控制
|
||||
|
||||
> 适合二次开发为企业内部管理平台。`
|
||||
},
|
||||
{
|
||||
id: 'erp-warehouse',
|
||||
name: 'ERP 仓储管理系统',
|
||||
category: 'enterprise',
|
||||
coverText: 'ERP',
|
||||
summary: '提供采购、销售、入库、出库、盘点、供应商和库存报表模块的仓储源码。',
|
||||
price: '¥199',
|
||||
views: 1320,
|
||||
updateTime: '2026-05-09',
|
||||
tags: ['ERP', 'MyBatis-Plus', 'Excel'],
|
||||
modules: ['采购入库', '销售出库', '库存盘点', '供应商管理', '报表导出'],
|
||||
scenes: ['仓储管理', '进销存', '供应链后台'],
|
||||
descriptionMd: `# ERP 仓储管理系统
|
||||
|
||||
源码聚焦进销存和仓储管理,适合小型供应链、仓库和贸易公司快速搭建业务后台。
|
||||
|
||||
## 模块清单
|
||||
|
||||
- 采购订单、采购入库和供应商档案
|
||||
- 销售订单、销售出库和客户档案
|
||||
- 库存流水、库存盘点和预警规则
|
||||
- Excel 导入导出与库存报表`
|
||||
},
|
||||
{
|
||||
id: 'lowcode-form-platform',
|
||||
name: '低代码表单平台',
|
||||
category: 'lowcode',
|
||||
coverText: 'FORM',
|
||||
summary: '支持动态表单、拖拽布局、字段配置、数据字典和在线设计的低代码源码。',
|
||||
price: '¥499',
|
||||
views: 980,
|
||||
updateTime: '2026-05-06',
|
||||
tags: ['Vue3', 'Element Plus', '拖拽设计'],
|
||||
modules: ['表单设计器', '字段配置', '动态校验', '数据字典', '表单预览'],
|
||||
scenes: ['低代码平台', '动态表单', '配置化后台'],
|
||||
descriptionMd: `# 低代码表单平台
|
||||
|
||||
项目用于构建配置化表单和数据采集页面,包含设计器、字段配置、预览和提交数据管理。
|
||||
|
||||
## 能力范围
|
||||
|
||||
- 拖拽式表单组件编排
|
||||
- 字段属性、校验规则和选项配置
|
||||
- 字典数据联动
|
||||
- 表单预览与数据提交记录`
|
||||
},
|
||||
{
|
||||
id: 'microservice-auth-starter',
|
||||
name: '微服务权限脚手架',
|
||||
category: 'microservice',
|
||||
coverText: 'MS',
|
||||
summary: '集成网关、认证中心、系统服务、日志服务、文件服务和监控基础能力的脚手架源码。',
|
||||
price: '¥599',
|
||||
views: 2560,
|
||||
updateTime: '2026-05-03',
|
||||
tags: ['Spring Cloud', 'Nacos', 'Gateway'],
|
||||
modules: ['网关服务', '认证中心', '系统服务', '日志中心', '文件服务'],
|
||||
scenes: ['微服务基础架构', '权限平台', '多服务治理'],
|
||||
descriptionMd: `# 微服务权限脚手架
|
||||
|
||||
这套源码适合从单体后台升级到微服务架构的团队,预置认证、网关、系统权限和基础监控能力。
|
||||
|
||||
## 服务组成
|
||||
|
||||
- Gateway 网关统一鉴权和路由
|
||||
- Auth 认证中心
|
||||
- System 系统权限服务
|
||||
- Log 操作日志服务
|
||||
- File 文件服务
|
||||
|
||||
## 推荐用途
|
||||
|
||||
可作为企业级微服务后台的基础工程,按业务域继续拆分服务。`
|
||||
}
|
||||
]
|
||||
|
||||
export function listSourceCategories() {
|
||||
const usedCategories = new Set(sourceProjects.map((project) => project.category))
|
||||
return [
|
||||
{ value: 'all', label: '全部源码' },
|
||||
...sourceCategories.filter((category) => usedCategories.has(category.value))
|
||||
]
|
||||
}
|
||||
|
||||
export function findSourceProject(id) {
|
||||
return sourceProjects.find((project) => project.id === id) || null
|
||||
}
|
||||
|
||||
export function filterSourceProjects({ category = 'all', keyword = '' } = {}) {
|
||||
const normalizedKeyword = keyword.trim().toLowerCase()
|
||||
return sourceProjects.filter((project) => {
|
||||
const categoryMatched = category === 'all' || project.category === category
|
||||
if (!categoryMatched) return false
|
||||
if (!normalizedKeyword) return true
|
||||
|
||||
const searchText = [
|
||||
project.name,
|
||||
project.summary,
|
||||
...project.tags,
|
||||
...project.modules,
|
||||
...project.scenes
|
||||
].join(' ').toLowerCase()
|
||||
return searchText.includes(normalizedKeyword)
|
||||
})
|
||||
}
|
||||
46
RuoYi-Vue/easycode-web/src/data/sourceProjects.test.mjs
Normal file
46
RuoYi-Vue/easycode-web/src/data/sourceProjects.test.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { filterSourceProjects, findSourceProject, listSourceCategories, sourceProjects } from './sourceProjects.js'
|
||||
|
||||
test('sourceProjects exposes visible projects with required display fields', () => {
|
||||
assert.ok(sourceProjects.length >= 6)
|
||||
sourceProjects.forEach((project) => {
|
||||
assert.ok(project.id)
|
||||
assert.ok(project.name)
|
||||
assert.ok(project.summary)
|
||||
assert.ok(project.coverText)
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(project, 'sourceSize'), false)
|
||||
assert.ok(Array.isArray(project.tags))
|
||||
assert.ok(project.tags.length >= 3)
|
||||
assert.ok(project.descriptionMd.includes('# '))
|
||||
})
|
||||
})
|
||||
|
||||
test('listSourceCategories includes all plus project categories without duplicates', () => {
|
||||
const categories = listSourceCategories()
|
||||
|
||||
assert.equal(categories[0].value, 'all')
|
||||
assert.equal(categories[0].label, '全部源码')
|
||||
assert.deepEqual(new Set(categories.map((category) => category.value)).size, categories.length)
|
||||
assert.ok(categories.some((category) => category.value === 'enterprise'))
|
||||
assert.ok(categories.some((category) => category.value === 'mall'))
|
||||
})
|
||||
|
||||
test('findSourceProject returns project by id and falls back to null', () => {
|
||||
assert.equal(findSourceProject('dms-platform')?.name, '商用车 DMS 管理系统')
|
||||
assert.equal(findSourceProject('missing-project'), null)
|
||||
})
|
||||
|
||||
test('filterSourceProjects filters by category and keyword', () => {
|
||||
const mallProjects = filterSourceProjects({ category: 'mall' })
|
||||
assert.deepEqual(
|
||||
mallProjects.map((project) => project.id),
|
||||
['mall-order-system']
|
||||
)
|
||||
|
||||
const searchProjects = filterSourceProjects({ category: 'all', keyword: '审批' })
|
||||
assert.deepEqual(
|
||||
searchProjects.map((project) => project.id),
|
||||
['oa-approval-suite']
|
||||
)
|
||||
})
|
||||
@@ -5,12 +5,18 @@ import HomeView from '@/views/HomeView.vue'
|
||||
import LoginView from '@/views/LoginView.vue'
|
||||
import RegisterView from '@/views/RegisterView.vue'
|
||||
import GenerateView from '@/views/GenerateView.vue'
|
||||
import ProjectListView from '@/views/ProjectListView.vue'
|
||||
import PreviewView from '@/views/PreviewView.vue'
|
||||
import SourceDetailView from '@/views/SourceDetailView.vue'
|
||||
import SourceStoreView from '@/views/SourceStoreView.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'home', component: HomeView },
|
||||
{ path: '/login', name: 'login', component: LoginView },
|
||||
{ path: '/register', name: 'register', component: RegisterView },
|
||||
{ path: '/source-store', name: 'source-store', component: SourceStoreView },
|
||||
{ path: '/source-store/:sourceId', name: 'source-detail', component: SourceDetailView },
|
||||
{ path: '/projects', name: 'projects', component: ProjectListView, meta: { requiresAuth: true } },
|
||||
{ path: '/generate', name: 'generate', component: GenerateView, meta: { requiresAuth: true } },
|
||||
{
|
||||
path: '/project/:projectId/preview',
|
||||
|
||||
17
RuoYi-Vue/easycode-web/src/utils/download.js
Normal file
17
RuoYi-Vue/easycode-web/src/utils/download.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export function saveBlob(blob, filename) {
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export function sourceZipName(project) {
|
||||
const fallback = project?.projectId ? `project-${project.projectId}` : 'easycode-project'
|
||||
const rawName = project?.projectFileName || project?.projectName || fallback
|
||||
const safeName = String(rawName).trim().replace(/[\\/:*?"<>|]/g, '_') || fallback
|
||||
return `${safeName}-source.zip`
|
||||
}
|
||||
92
RuoYi-Vue/easycode-web/src/utils/erExport.js
Normal file
92
RuoYi-Vue/easycode-web/src/utils/erExport.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const HEADER_HEIGHT = 60
|
||||
const FIELD_HEIGHT = 28
|
||||
|
||||
function escapeXml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function truncate(value, maxLength) {
|
||||
const text = String(value ?? '')
|
||||
return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text
|
||||
}
|
||||
|
||||
function badgeOf(field) {
|
||||
if (field.primary) {
|
||||
return { text: 'PK', fill: '#dbeafe', color: '#1d4ed8' }
|
||||
}
|
||||
if (field.required) {
|
||||
return { text: '*', fill: '#fef3c7', color: '#b45309' }
|
||||
}
|
||||
return { text: '-', fill: '#ffffff', color: '#cbd5e1' }
|
||||
}
|
||||
|
||||
function renderEdge(edge) {
|
||||
return `
|
||||
<path class="relation-line${edge.inferred ? '' : ' manual'}" d="${escapeXml(edge.path)}" marker-end="url(#er-arrow)" />
|
||||
<text class="relation-label" x="${edge.labelX}" y="${edge.labelY}">${escapeXml(edge.fieldName)}</text>`
|
||||
}
|
||||
|
||||
function renderField(field, index) {
|
||||
const y = HEADER_HEIGHT + index * FIELD_HEIGHT + 20
|
||||
const badge = badgeOf(field)
|
||||
return `
|
||||
<rect x="12" y="${y - 14}" width="26" height="18" rx="4" fill="${badge.fill}" />
|
||||
<text x="25" y="${y}" class="field-badge" fill="${badge.color}">${escapeXml(badge.text)}</text>
|
||||
<text x="48" y="${y}" class="field-name">${escapeXml(truncate(field.name, 18))}</text>
|
||||
<text x="152" y="${y}" class="field-comment">${escapeXml(truncate(field.comment, 12))}</text>`
|
||||
}
|
||||
|
||||
function renderNode(node) {
|
||||
const fields = node.fields.length
|
||||
? node.fields.map(renderField).join('')
|
||||
: '<text x="130" y="92" class="field-empty">No fields</text>'
|
||||
|
||||
return `
|
||||
<g transform="translate(${node.x}, ${node.y})">
|
||||
<rect class="entity-box" width="${node.width}" height="${node.height}" rx="8" />
|
||||
<rect class="entity-head-bg" width="${node.width}" height="${HEADER_HEIGHT}" rx="8" />
|
||||
<rect x="0" y="52" width="${node.width}" height="8" fill="#eef5ff" />
|
||||
<line x1="0" y1="${HEADER_HEIGHT}" x2="${node.width}" y2="${HEADER_HEIGHT}" class="entity-divider" />
|
||||
<text x="14" y="27" class="entity-title">${escapeXml(truncate(node.title, 24))}</text>
|
||||
<text x="14" y="47" class="entity-comment">${escapeXml(truncate(node.comment, 26))}</text>
|
||||
${fields}
|
||||
</g>`
|
||||
}
|
||||
|
||||
export function buildErExportSvg({ width, height, nodes, edges }) {
|
||||
const canvasWidth = Math.max(1, Number(width) || 1)
|
||||
const canvasHeight = Math.max(1, Number(height) || 1)
|
||||
const exportNodes = Array.isArray(nodes) ? nodes : []
|
||||
const exportEdges = Array.isArray(edges) ? edges : []
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${canvasWidth}" height="${canvasHeight}" viewBox="0 0 ${canvasWidth} ${canvasHeight}">
|
||||
<defs>
|
||||
<marker id="er-arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M0,0 L0,6 L8,3 z" fill="#8aa0bc" />
|
||||
</marker>
|
||||
<style>
|
||||
.relation-line{fill:none;stroke:#8aa0bc;stroke-width:1.6;stroke-dasharray:6 5}
|
||||
.relation-line.manual{stroke:#2563eb;stroke-dasharray:none}
|
||||
.relation-label{fill:#64748b;font-family:Arial,sans-serif;font-size:12px;paint-order:stroke;stroke:#f8fafc;stroke-width:4px;text-anchor:middle}
|
||||
.entity-box{fill:#fff;stroke:#d6e0ee;stroke-width:1}
|
||||
.entity-head-bg{fill:#eef5ff}
|
||||
.entity-divider{stroke:#e7edf5;stroke-width:1}
|
||||
.entity-title{fill:#0f172a;font-family:Arial,sans-serif;font-size:15px;font-weight:700}
|
||||
.entity-comment{fill:#64748b;font-family:Arial,sans-serif;font-size:12px}
|
||||
.field-badge{font-family:Arial,sans-serif;font-size:11px;font-weight:700;text-anchor:middle}
|
||||
.field-name{fill:#334155;font-family:Arial,sans-serif;font-size:13px}
|
||||
.field-comment{fill:#94a3b8;font-family:Arial,sans-serif;font-size:12px}
|
||||
.field-empty{fill:#94a3b8;font-family:Arial,sans-serif;font-size:13px;text-anchor:middle}
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#f8fafc" />
|
||||
<g>${exportEdges.map(renderEdge).join('')}</g>
|
||||
<g>${exportNodes.map(renderNode).join('')}</g>
|
||||
</svg>`
|
||||
}
|
||||
41
RuoYi-Vue/easycode-web/src/utils/erExport.test.mjs
Normal file
41
RuoYi-Vue/easycode-web/src/utils/erExport.test.mjs
Normal file
@@ -0,0 +1,41 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildErExportSvg } from './erExport.js'
|
||||
|
||||
test('buildErExportSvg renders a pure SVG that can be drawn to canvas safely', () => {
|
||||
const svg = buildErExportSvg({
|
||||
width: 900,
|
||||
height: 480,
|
||||
nodes: [
|
||||
{
|
||||
id: 'student_info',
|
||||
title: 'student_info',
|
||||
comment: 'Student table',
|
||||
x: 20,
|
||||
y: 30,
|
||||
width: 260,
|
||||
height: 132,
|
||||
fields: [
|
||||
{ name: 'id', comment: 'ID', primary: true, required: true },
|
||||
{ name: 'department_id', comment: 'Department', primary: false, required: false }
|
||||
]
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'student_info.department_id->department.id',
|
||||
path: 'M 280 120 C 340 120, 420 120, 480 120',
|
||||
labelX: 380,
|
||||
labelY: 112,
|
||||
fieldName: 'department_id',
|
||||
inferred: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
assert.match(svg, /^<\?xml version="1.0" encoding="UTF-8"\?>/)
|
||||
assert.equal(svg.includes('<foreignObject'), false)
|
||||
assert.equal(svg.includes('xmlns="http://www.w3.org/2000/svg"'), true)
|
||||
assert.equal(svg.includes('student_info'), true)
|
||||
assert.equal(svg.includes('department_id'), true)
|
||||
})
|
||||
154
RuoYi-Vue/easycode-web/src/utils/erGraph.js
Normal file
154
RuoYi-Vue/easycode-web/src/utils/erGraph.js
Normal file
@@ -0,0 +1,154 @@
|
||||
const NODE_WIDTH = 260
|
||||
const HEADER_HEIGHT = 60
|
||||
const FIELD_HEIGHT = 28
|
||||
const NODE_GAP_X = 80
|
||||
const NODE_GAP_Y = 72
|
||||
const MIN_NODE_HEIGHT = 132
|
||||
|
||||
function flagOn(value) {
|
||||
return value === true || value === 1 || value === '1' || value === 'true'
|
||||
}
|
||||
|
||||
function cleanName(value, fallback) {
|
||||
const text = String(value || '').trim()
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
function normalizeKey(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '')
|
||||
}
|
||||
|
||||
function tableIdOf(table, index) {
|
||||
return cleanName(table?.tableName || table?.id || table?.tableId, `table_${index + 1}`)
|
||||
}
|
||||
|
||||
function buildField(column, index) {
|
||||
return {
|
||||
id: cleanName(column?.columnName || column?.id || column?.columnId, `field_${index + 1}`),
|
||||
name: cleanName(column?.columnName, `field_${index + 1}`),
|
||||
comment: String(column?.columnComment || '').trim(),
|
||||
type: String(column?.columnType || '').trim(),
|
||||
primary: flagOn(column?.isPk),
|
||||
required: flagOn(column?.isRequired)
|
||||
}
|
||||
}
|
||||
|
||||
function buildNode(table, index, columns) {
|
||||
const fields = columns.map(buildField)
|
||||
const x = (index % 3) * (NODE_WIDTH + NODE_GAP_X)
|
||||
const y = Math.floor(index / 3) * (MIN_NODE_HEIGHT + NODE_GAP_Y)
|
||||
const height = Math.max(MIN_NODE_HEIGHT, HEADER_HEIGHT + fields.length * FIELD_HEIGHT + 18)
|
||||
|
||||
return {
|
||||
id: tableIdOf(table, index),
|
||||
title: cleanName(table?.tableName, `table_${index + 1}`),
|
||||
comment: String(table?.tableComment || '').trim(),
|
||||
x,
|
||||
y,
|
||||
width: NODE_WIDTH,
|
||||
height,
|
||||
fields
|
||||
}
|
||||
}
|
||||
|
||||
function savedPositionFor(draft, nodeId) {
|
||||
const position = draft?.positions?.[nodeId]
|
||||
const x = Number(position?.x)
|
||||
const y = Number(position?.y)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||
return null
|
||||
}
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
function targetCandidates(base) {
|
||||
return [base, `${base}_info`, `${base}_table`, `${base}s`].map(normalizeKey)
|
||||
}
|
||||
|
||||
function findTargetNode(nodes, base, sourceId) {
|
||||
const candidates = targetCandidates(base)
|
||||
const exact = nodes.find((node) => node.id !== sourceId && candidates.includes(normalizeKey(node.title)))
|
||||
if (exact) return exact
|
||||
|
||||
return nodes.find((node) => {
|
||||
if (node.id === sourceId) return false
|
||||
const nodeKey = normalizeKey(node.title)
|
||||
return nodeKey.startsWith(normalizeKey(`${base}_`)) || nodeKey.startsWith(normalizeKey(base))
|
||||
})
|
||||
}
|
||||
|
||||
function primaryFieldOf(node) {
|
||||
return node.fields.find((field) => field.primary) || node.fields.find((field) => field.name === 'id') || node.fields[0]
|
||||
}
|
||||
|
||||
function buildInferredEdges(nodes, deletedEdgeIds) {
|
||||
const edges = []
|
||||
const deleted = new Set(Array.isArray(deletedEdgeIds) ? deletedEdgeIds : [])
|
||||
|
||||
nodes.forEach((sourceNode) => {
|
||||
sourceNode.fields.forEach((field) => {
|
||||
const fieldName = String(field.name || '').toLowerCase()
|
||||
if (field.primary || !fieldName.endsWith('_id')) return
|
||||
|
||||
const base = fieldName.slice(0, -3)
|
||||
if (!base) return
|
||||
|
||||
const targetNode = findTargetNode(nodes, base, sourceNode.id)
|
||||
if (!targetNode) return
|
||||
|
||||
const targetField = primaryFieldOf(targetNode)
|
||||
const id = `${sourceNode.id}.${field.name}->${targetNode.id}.${targetField?.name || 'id'}`
|
||||
if (deleted.has(id)) return
|
||||
|
||||
edges.push({
|
||||
id,
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id,
|
||||
fieldName: field.name,
|
||||
targetFieldName: targetField?.name || 'id',
|
||||
inferred: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return edges
|
||||
}
|
||||
|
||||
function buildManualEdges(nodes, relations) {
|
||||
const nodeIds = new Set(nodes.map((node) => node.id))
|
||||
return (Array.isArray(relations) ? relations : [])
|
||||
.filter((relation) => nodeIds.has(relation?.source) && nodeIds.has(relation?.target))
|
||||
.map((relation, index) => ({
|
||||
id: cleanName(relation.id, `manual_${index + 1}`),
|
||||
source: relation.source,
|
||||
target: relation.target,
|
||||
fieldName: cleanName(relation.fieldName, 'id'),
|
||||
targetFieldName: cleanName(relation.targetFieldName, 'id'),
|
||||
inferred: false
|
||||
}))
|
||||
}
|
||||
|
||||
function applyPositions(nodes, draft) {
|
||||
return nodes.map((node) => {
|
||||
const position = savedPositionFor(draft, node.id)
|
||||
return position ? { ...node, ...position } : node
|
||||
})
|
||||
}
|
||||
|
||||
export function buildErGraph(tables, draft = {}) {
|
||||
const sourceTables = Array.isArray(tables) ? tables : []
|
||||
const nodes = applyPositions(
|
||||
sourceTables.map((table, index) => buildNode(table, index, Array.isArray(table?.columns) ? table.columns : [])),
|
||||
draft
|
||||
)
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges: [
|
||||
...buildInferredEdges(nodes, draft?.deletedEdgeIds),
|
||||
...buildManualEdges(nodes, draft?.relations)
|
||||
]
|
||||
}
|
||||
}
|
||||
95
RuoYi-Vue/easycode-web/src/utils/erGraph.test.mjs
Normal file
95
RuoYi-Vue/easycode-web/src/utils/erGraph.test.mjs
Normal file
@@ -0,0 +1,95 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildErGraph } from './erGraph.js'
|
||||
|
||||
test('buildErGraph creates table nodes with primary and required fields', () => {
|
||||
const graph = buildErGraph([
|
||||
{
|
||||
tableName: 'department',
|
||||
tableComment: '院系表',
|
||||
columns: [
|
||||
{ columnName: 'id', columnComment: '主键ID', isPk: '1', isRequired: '1' },
|
||||
{ columnName: 'dept_name', columnComment: '院系名称', isRequired: '1' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
assert.equal(graph.nodes.length, 1)
|
||||
assert.equal(graph.nodes[0].title, 'department')
|
||||
assert.equal(graph.nodes[0].comment, '院系表')
|
||||
assert.deepEqual(
|
||||
graph.nodes[0].fields.map((field) => ({ name: field.name, primary: field.primary, required: field.required })),
|
||||
[
|
||||
{ name: 'id', primary: true, required: true },
|
||||
{ name: 'dept_name', primary: false, required: true }
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('buildErGraph infers foreign-key style edges from xxx_id fields', () => {
|
||||
const graph = buildErGraph([
|
||||
{
|
||||
tableName: 'department',
|
||||
columns: [{ columnName: 'id', isPk: '1' }]
|
||||
},
|
||||
{
|
||||
tableName: 'student_info',
|
||||
columns: [
|
||||
{ columnName: 'id', isPk: '1' },
|
||||
{ columnName: 'department_id', columnComment: '所属院系' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
assert.equal(graph.edges.length, 1)
|
||||
assert.deepEqual(
|
||||
{
|
||||
source: graph.edges[0].source,
|
||||
target: graph.edges[0].target,
|
||||
fieldName: graph.edges[0].fieldName,
|
||||
targetFieldName: graph.edges[0].targetFieldName
|
||||
},
|
||||
{
|
||||
source: 'student_info',
|
||||
target: 'department',
|
||||
fieldName: 'department_id',
|
||||
targetFieldName: 'id'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('buildErGraph applies saved node positions', () => {
|
||||
const graph = buildErGraph(
|
||||
[{ tableName: 'department', columns: [{ columnName: 'id', isPk: '1' }] }],
|
||||
{ positions: { department: { x: 120, y: 240 } } }
|
||||
)
|
||||
|
||||
assert.equal(graph.nodes[0].x, 120)
|
||||
assert.equal(graph.nodes[0].y, 240)
|
||||
})
|
||||
|
||||
test('buildErGraph merges manual relations and hides deleted inferred relations', () => {
|
||||
const graph = buildErGraph(
|
||||
[
|
||||
{ tableName: 'department', columns: [{ columnName: 'id', isPk: '1' }] },
|
||||
{ tableName: 'student_info', columns: [{ columnName: 'id', isPk: '1' }, { columnName: 'department_id' }] },
|
||||
{ tableName: 'class_info', columns: [{ columnName: 'id', isPk: '1' }] }
|
||||
],
|
||||
{
|
||||
deletedEdgeIds: ['student_info.department_id->department.id'],
|
||||
relations: [
|
||||
{
|
||||
id: 'manual_student_class',
|
||||
source: 'student_info',
|
||||
target: 'class_info',
|
||||
fieldName: 'id',
|
||||
targetFieldName: 'id'
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
assert.equal(graph.edges.length, 1)
|
||||
assert.equal(graph.edges[0].id, 'manual_student_class')
|
||||
assert.equal(graph.edges[0].inferred, false)
|
||||
})
|
||||
148
RuoYi-Vue/easycode-web/src/utils/markdown.js
Normal file
148
RuoYi-Vue/easycode-web/src/utils/markdown.js
Normal file
@@ -0,0 +1,148 @@
|
||||
const escapeMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>"']/g, (char) => escapeMap[char])
|
||||
}
|
||||
|
||||
function renderInline(value) {
|
||||
let html = escapeHtml(value)
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
||||
return html
|
||||
}
|
||||
|
||||
function renderTable(rows) {
|
||||
if (rows.length < 2) return ''
|
||||
const headers = splitTableRow(rows[0])
|
||||
const bodyRows = rows.slice(2).map(splitTableRow)
|
||||
const headerHtml = headers.map((header) => `<th>${renderInline(header)}</th>`).join('')
|
||||
const bodyHtml = bodyRows
|
||||
.map((row) => `<tr>${row.map((cell) => `<td>${renderInline(cell)}</td>`).join('')}</tr>`)
|
||||
.join('')
|
||||
|
||||
return `<table><thead><tr>${headerHtml}</tr></thead><tbody>${bodyHtml}</tbody></table>`
|
||||
}
|
||||
|
||||
function splitTableRow(row) {
|
||||
return row
|
||||
.trim()
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
.split('|')
|
||||
.map((cell) => cell.trim())
|
||||
}
|
||||
|
||||
function isTableDivider(line) {
|
||||
return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line)
|
||||
}
|
||||
|
||||
function flushParagraph(parts, output) {
|
||||
if (!parts.length) return
|
||||
output.push(`<p>${renderInline(parts.join(' '))}</p>`)
|
||||
parts.length = 0
|
||||
}
|
||||
|
||||
function flushList(items, output) {
|
||||
if (!items.length) return
|
||||
output.push(`<ul>${items.map((item) => `<li>${renderInline(item)}</li>`).join('')}</ul>`)
|
||||
items.length = 0
|
||||
}
|
||||
|
||||
function flushTable(rows, output) {
|
||||
if (!rows.length) return
|
||||
const html = renderTable(rows)
|
||||
if (html) {
|
||||
output.push(html)
|
||||
}
|
||||
rows.length = 0
|
||||
}
|
||||
|
||||
export function renderMarkdown(markdown) {
|
||||
const lines = String(markdown || '').replace(/\r\n/g, '\n').split('\n')
|
||||
const output = []
|
||||
const paragraph = []
|
||||
const listItems = []
|
||||
const tableRows = []
|
||||
let codeLanguage = ''
|
||||
let codeLines = []
|
||||
|
||||
function flushBlocks() {
|
||||
flushParagraph(paragraph, output)
|
||||
flushList(listItems, output)
|
||||
flushTable(tableRows, output)
|
||||
}
|
||||
|
||||
lines.forEach((line) => {
|
||||
const fenceMatch = line.match(/^```(\w+)?\s*$/)
|
||||
if (fenceMatch) {
|
||||
if (codeLanguage || codeLines.length) {
|
||||
const languageClass = codeLanguage ? ` class="language-${escapeHtml(codeLanguage)}"` : ''
|
||||
output.push(`<pre><code${languageClass}>${escapeHtml(`${codeLines.join('\n')}\n`)}</code></pre>`)
|
||||
codeLanguage = ''
|
||||
codeLines = []
|
||||
} else {
|
||||
flushBlocks()
|
||||
codeLanguage = fenceMatch[1] || 'text'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (codeLanguage || codeLines.length) {
|
||||
codeLines.push(line)
|
||||
return
|
||||
}
|
||||
|
||||
if (!line.trim()) {
|
||||
flushBlocks()
|
||||
return
|
||||
}
|
||||
|
||||
if (tableRows.length || (line.includes('|') && lines[lines.indexOf(line) + 1] && isTableDivider(lines[lines.indexOf(line) + 1]))) {
|
||||
flushParagraph(paragraph, output)
|
||||
flushList(listItems, output)
|
||||
tableRows.push(line)
|
||||
return
|
||||
}
|
||||
|
||||
const headingMatch = line.match(/^(#{1,4})\s+(.+)$/)
|
||||
if (headingMatch) {
|
||||
flushBlocks()
|
||||
const level = headingMatch[1].length
|
||||
output.push(`<h${level}>${renderInline(headingMatch[2])}</h${level}>`)
|
||||
return
|
||||
}
|
||||
|
||||
const listMatch = line.match(/^\s*[-*]\s+(.+)$/)
|
||||
if (listMatch) {
|
||||
flushParagraph(paragraph, output)
|
||||
listItems.push(listMatch[1])
|
||||
return
|
||||
}
|
||||
|
||||
const quoteMatch = line.match(/^>\s+(.+)$/)
|
||||
if (quoteMatch) {
|
||||
flushBlocks()
|
||||
output.push(`<blockquote>${renderInline(quoteMatch[1])}</blockquote>`)
|
||||
return
|
||||
}
|
||||
|
||||
flushList(listItems, output)
|
||||
flushTable(tableRows, output)
|
||||
paragraph.push(line.trim())
|
||||
})
|
||||
|
||||
if (codeLanguage || codeLines.length) {
|
||||
const languageClass = codeLanguage ? ` class="language-${escapeHtml(codeLanguage)}"` : ''
|
||||
output.push(`<pre><code${languageClass}>${escapeHtml(`${codeLines.join('\n')}\n`)}</code></pre>`)
|
||||
}
|
||||
flushBlocks()
|
||||
|
||||
return output.join('\n')
|
||||
}
|
||||
22
RuoYi-Vue/easycode-web/src/utils/markdown.test.mjs
Normal file
22
RuoYi-Vue/easycode-web/src/utils/markdown.test.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { renderMarkdown } from './markdown.js'
|
||||
|
||||
test('renderMarkdown renders common markdown and keeps raw html escaped', () => {
|
||||
const html = renderMarkdown(`# 项目介绍
|
||||
|
||||
- Spring Boot
|
||||
- Vue3
|
||||
|
||||
\`\`\`bash
|
||||
npm run dev
|
||||
\`\`\`
|
||||
|
||||
<script>alert('xss')</script>`)
|
||||
|
||||
assert.match(html, /<h1>项目介绍<\/h1>/)
|
||||
assert.match(html, /<li>Spring Boot<\/li>/)
|
||||
assert.match(html, /<pre><code class="language-bash">npm run dev\n<\/code><\/pre>/)
|
||||
assert.equal(html.includes('<script>'), false)
|
||||
assert.ok(html.includes('<script>alert'))
|
||||
})
|
||||
@@ -1,15 +1,11 @@
|
||||
<template>
|
||||
<section class="page generate-page">
|
||||
<div class="panel">
|
||||
<div class="panel" v-loading="loadingProject">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h1 class="panel-title">项目生成工作台</h1>
|
||||
<p class="muted">先生成数据库结构,确认后再进入项目预览。</p>
|
||||
</div>
|
||||
<el-steps class="steps" :active="activeStep" finish-status="success" simple>
|
||||
<el-step title="生成数据库" />
|
||||
<el-step title="预览项目" />
|
||||
</el-steps>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
@@ -69,34 +65,45 @@
|
||||
</div>
|
||||
|
||||
<div class="panel designer-panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-header database-header">
|
||||
<h2 class="panel-title">数据库设计</h2>
|
||||
<span class="muted">表数量:{{ database.tables.length }}</span>
|
||||
<div class="database-header-actions">
|
||||
<el-radio-group v-model="databaseView" size="large">
|
||||
<el-radio-button label="editor">表结构编辑</el-radio-button>
|
||||
<el-radio-button label="er">ER 图</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="muted">表数量:{{ database.tables.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<DatabaseDesigner v-if="database.tables.length" v-model="database" />
|
||||
<template v-if="database.tables.length">
|
||||
<DatabaseDesigner v-show="databaseView === 'editor'" v-model="database" />
|
||||
<ErDiagramView v-show="databaseView === 'er'" :tables="database.tables" v-model:draft="database.erDiagram" />
|
||||
</template>
|
||||
<el-empty v-else description="点击生成数据库后,这里会展示可编辑表结构" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { DataAnalysis, View } from '@element-plus/icons-vue'
|
||||
import DatabaseDesigner from '@/components/DatabaseDesigner.vue'
|
||||
import { createProject, generateDatabase, generateProject, saveDatabase } from '@/api/project'
|
||||
import ErDiagramView from '@/components/ErDiagramView.vue'
|
||||
import { createProject, generateDatabase, generateProject, getDatabase, getProject, saveDatabase } from '@/api/project'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const activeStep = ref(0)
|
||||
const generating = ref(false)
|
||||
const saving = ref(false)
|
||||
const previewing = ref(false)
|
||||
const projectId = ref(null)
|
||||
const loadingProject = ref(false)
|
||||
const databaseView = ref('editor')
|
||||
const projectId = ref(normalizeProjectId(route.query.projectId))
|
||||
const errorMessage = ref('')
|
||||
const database = ref({ tables: [], sql: '' })
|
||||
const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram() })
|
||||
const projectForm = reactive({
|
||||
projectName: '',
|
||||
keyword: String(route.query.keyword || ''),
|
||||
@@ -104,15 +111,69 @@ const projectForm = reactive({
|
||||
industryTemplate: ''
|
||||
})
|
||||
|
||||
const LAST_PROJECT_KEY = 'easycode_last_project_id'
|
||||
|
||||
function unwrap(result) {
|
||||
return result?.data || result || {}
|
||||
}
|
||||
|
||||
function emptyErDiagram() {
|
||||
return {
|
||||
positions: {},
|
||||
relations: [],
|
||||
deletedEdgeIds: []
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeErDiagram(value) {
|
||||
const source = value && typeof value === 'object' ? value : {}
|
||||
return {
|
||||
positions: source.positions && typeof source.positions === 'object' ? source.positions : {},
|
||||
relations: Array.isArray(source.relations) ? source.relations : [],
|
||||
deletedEdgeIds: Array.isArray(source.deletedEdgeIds) ? source.deletedEdgeIds : []
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDatabase(payload) {
|
||||
const data = unwrap(payload)
|
||||
return {
|
||||
tables: Array.isArray(data.tables) ? data.tables : [],
|
||||
sql: data.sql || ''
|
||||
sql: data.sql || '',
|
||||
erDiagram: normalizeErDiagram(data.erDiagram)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeProjectId(value) {
|
||||
const raw = Array.isArray(value) ? value[0] : value
|
||||
const id = Number(raw)
|
||||
return Number.isFinite(id) && id > 0 ? String(id) : ''
|
||||
}
|
||||
|
||||
function rememberProject(id) {
|
||||
if (id) {
|
||||
localStorage.setItem(LAST_PROJECT_KEY, String(id))
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjectDraft(value) {
|
||||
const id = normalizeProjectId(value)
|
||||
if (!id) return
|
||||
|
||||
loadingProject.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const [projectResult, databaseResult] = await Promise.all([getProject(id), getDatabase(id)])
|
||||
const project = unwrap(projectResult)
|
||||
projectId.value = id
|
||||
projectForm.projectName = project.projectName || ''
|
||||
projectForm.projectDesc = project.projectDesc || ''
|
||||
projectForm.industryTemplate = project.industryTemplate || ''
|
||||
database.value = normalizeDatabase(databaseResult)
|
||||
rememberProject(id)
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '加载项目草稿失败,请从我的项目重新打开'
|
||||
} finally {
|
||||
loadingProject.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +192,15 @@ async function ensureProject() {
|
||||
throw new Error('创建项目成功但未返回项目 ID')
|
||||
}
|
||||
|
||||
rememberProject(projectId.value)
|
||||
router.replace({
|
||||
path: '/generate',
|
||||
query: {
|
||||
...route.query,
|
||||
projectId: projectId.value
|
||||
}
|
||||
})
|
||||
|
||||
return projectId.value
|
||||
}
|
||||
|
||||
@@ -161,7 +231,6 @@ async function handleGenerateDatabase() {
|
||||
errorMessage.value = '后端暂未返回表结构,请稍后重试或手动添加表。'
|
||||
}
|
||||
|
||||
activeStep.value = 1
|
||||
ElMessage.success('数据库结构生成完成')
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '生成数据库失败,请确认后端服务已启动。'
|
||||
@@ -196,6 +265,22 @@ async function handlePreview() {
|
||||
previewing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query.projectId,
|
||||
(value) => {
|
||||
const id = normalizeProjectId(value)
|
||||
if (id && id !== projectId.value) {
|
||||
loadProjectDraft(id)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (projectId.value) {
|
||||
loadProjectDraft(projectId.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -204,10 +289,6 @@ async function handlePreview() {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.steps {
|
||||
width: min(430px, 100%);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
@@ -226,10 +307,25 @@ async function handlePreview() {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.database-header {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.database-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.panel-header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.database-header-actions {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
<el-button size="large" type="primary" :icon="MagicStick" @click="handleGenerate">
|
||||
立即生成
|
||||
</el-button>
|
||||
<el-button size="large" @click="router.push('/login')">登录</el-button>
|
||||
</el-form>
|
||||
|
||||
<div class="status-strip">
|
||||
@@ -95,7 +94,7 @@ function handleGenerate() {
|
||||
|
||||
.quick-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 1fr) auto auto;
|
||||
grid-template-columns: minmax(240px, 1fr) auto;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
<aside class="structure-pane">
|
||||
<div class="panel-header">
|
||||
<h1 class="panel-title">项目结构</h1>
|
||||
<el-button :icon="Refresh" :loading="loading" @click="loadStructures">刷新</el-button>
|
||||
<div class="toolbar">
|
||||
<el-button :icon="Refresh" :loading="loading" @click="loadStructures">刷新</el-button>
|
||||
<el-button type="primary" :icon="Download" :loading="downloading" @click="downloadSource">下载源码</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="activeType" class="type-tabs">
|
||||
<el-tab-pane label="后端" name="backend" />
|
||||
@@ -26,16 +29,18 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { Download, Refresh } from '@element-plus/icons-vue'
|
||||
import CodePreview from '@/components/CodePreview.vue'
|
||||
import ProjectStructureTree from '@/components/ProjectStructureTree.vue'
|
||||
import { getFileContent, getProjectStructure } from '@/api/project'
|
||||
import { downloadProject, getFileContent, getProjectStructure } from '@/api/project'
|
||||
import { saveBlob, sourceZipName } from '@/utils/download'
|
||||
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => route.params.projectId)
|
||||
const activeType = ref('backend')
|
||||
const loading = ref(false)
|
||||
const codeLoading = ref(false)
|
||||
const downloading = ref(false)
|
||||
const code = ref('')
|
||||
const selectedFileName = ref('')
|
||||
const structures = reactive({
|
||||
@@ -112,6 +117,19 @@ async function handleFileSelect(file) {
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadSource() {
|
||||
downloading.value = true
|
||||
try {
|
||||
const blob = await downloadProject(projectId.value)
|
||||
saveBlob(blob, sourceZipName({ projectId: projectId.value }))
|
||||
ElMessage.success('源码下载已开始')
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '下载源码失败')
|
||||
} finally {
|
||||
downloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeType, () => {
|
||||
code.value = ''
|
||||
selectedFileName.value = ''
|
||||
|
||||
225
RuoYi-Vue/easycode-web/src/views/ProjectListView.vue
Normal file
225
RuoYi-Vue/easycode-web/src/views/ProjectListView.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<section class="page projects-page">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h1 class="panel-title">我的项目</h1>
|
||||
<p class="muted">继续编辑已创建的项目草稿,或打开已经生成的项目预览。</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<el-button :icon="Refresh" :loading="loading" @click="loadProjects">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="router.push('/generate')">新建项目</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<el-table v-loading="loading" :data="projects" class="project-table">
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="project-name">{{ row.projectName || '未命名项目' }}</div>
|
||||
<div class="project-desc">{{ row.projectDesc || row.industryTemplate || '暂无描述' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusMeta(row).type" effect="light">
|
||||
{{ statusMeta(row).label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updateTime" label="更新时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.updateTime || row.createTime || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="390" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-button class="action-button action-button-wide" :icon="EditPen" plain @click="continueProject(row)">
|
||||
继续生成
|
||||
</el-button>
|
||||
<el-button
|
||||
class="action-button"
|
||||
:icon="View"
|
||||
plain
|
||||
:disabled="row.previewStatus !== '1'"
|
||||
@click="router.push(`/project/${projectIdOf(row)}/preview`)"
|
||||
>
|
||||
预览
|
||||
</el-button>
|
||||
<el-button
|
||||
class="action-button"
|
||||
:icon="Download"
|
||||
plain
|
||||
:disabled="row.previewStatus !== '1'"
|
||||
:loading="downloadingProjectId === projectIdOf(row)"
|
||||
@click="downloadSource(row)"
|
||||
>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button class="action-button" :icon="Delete" type="danger" plain @click="removeProject(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-empty
|
||||
v-if="!loading && !projects.length"
|
||||
description="暂无项目,创建一个新的项目草稿吧"
|
||||
>
|
||||
<el-button type="primary" :icon="Plus" @click="router.push('/generate')">新建项目</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Download, EditPen, Plus, Refresh, View } from '@element-plus/icons-vue'
|
||||
import { deleteProject, downloadProject, listProjects } from '@/api/project'
|
||||
import { saveBlob, sourceZipName } from '@/utils/download'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const downloadingProjectId = ref(null)
|
||||
const projects = ref([])
|
||||
|
||||
function projectIdOf(project) {
|
||||
return project?.projectId || project?.id
|
||||
}
|
||||
|
||||
function statusMeta(project) {
|
||||
if (project?.previewStatus === '1') {
|
||||
return { label: '已预览', type: 'success' }
|
||||
}
|
||||
if (project?.generateStatus === '1') {
|
||||
return { label: '已生成数据库', type: 'primary' }
|
||||
}
|
||||
return { label: '草稿', type: 'info' }
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await listProjects()
|
||||
projects.value = Array.isArray(result) ? result : []
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '加载项目列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function continueProject(project) {
|
||||
const projectId = projectIdOf(project)
|
||||
if (!projectId) {
|
||||
ElMessage.warning('项目 ID 不存在,无法继续')
|
||||
return
|
||||
}
|
||||
router.push({ path: '/generate', query: { projectId } })
|
||||
}
|
||||
|
||||
async function downloadSource(project) {
|
||||
const projectId = projectIdOf(project)
|
||||
if (!projectId) return
|
||||
if (project.previewStatus !== '1') {
|
||||
ElMessage.warning('请先完成项目预览')
|
||||
return
|
||||
}
|
||||
|
||||
downloadingProjectId.value = projectId
|
||||
try {
|
||||
const blob = await downloadProject(projectId)
|
||||
saveBlob(blob, sourceZipName(project))
|
||||
ElMessage.success('源码下载已开始')
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '下载源码失败')
|
||||
} finally {
|
||||
downloadingProjectId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function removeProject(project) {
|
||||
const projectId = projectIdOf(project)
|
||||
if (!projectId) return
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除项目「${project.projectName || projectId}」?`, '删除项目', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
await deleteProject(projectId)
|
||||
ElMessage.success('项目已删除')
|
||||
await loadProjects()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
ElMessage.error(error.message || '删除项目失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadProjects)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.projects-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px 20px 22px;
|
||||
}
|
||||
|
||||
.project-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
color: #172033;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.project-desc {
|
||||
margin-top: 4px;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row-actions :deep(.el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
min-width: 72px;
|
||||
height: 32px;
|
||||
padding: 7px 10px;
|
||||
border-color: #d8e0eb;
|
||||
color: #475467;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.action-button-wide {
|
||||
min-width: 94px;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.panel-header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
403
RuoYi-Vue/easycode-web/src/views/SourceDetailView.vue
Normal file
403
RuoYi-Vue/easycode-web/src/views/SourceDetailView.vue
Normal file
@@ -0,0 +1,403 @@
|
||||
<template>
|
||||
<section class="page source-detail-page" v-loading="loading">
|
||||
<template v-if="project">
|
||||
<div class="detail-hero">
|
||||
<el-button class="back-button" :icon="Back" plain @click="router.push('/source-store')">返回源码库</el-button>
|
||||
<div class="hero-main">
|
||||
<div class="hero-cover">{{ project.coverText }}</div>
|
||||
<div class="hero-copy">
|
||||
<div class="hero-title-row">
|
||||
<div>
|
||||
<p class="eyebrow">源码详情</p>
|
||||
<h1>{{ project.name }}</h1>
|
||||
</div>
|
||||
<strong>{{ project.price }}</strong>
|
||||
</div>
|
||||
<p class="muted">{{ project.summary }}</p>
|
||||
<div class="tag-row">
|
||||
<el-tag v-for="tag in project.tags" :key="tag" effect="plain">{{ tag }}</el-tag>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<el-button
|
||||
:icon="project.purchased ? Download : ShoppingCart"
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="project.purchased ? handleDownload() : handlePurchase()"
|
||||
>
|
||||
{{ project.purchased ? '下载源码' : '购买源码' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-layout">
|
||||
<article class="markdown-panel">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">项目描述</h2>
|
||||
</div>
|
||||
<div class="markdown-body" v-html="descriptionHtml"></div>
|
||||
</article>
|
||||
|
||||
<aside class="summary-panel">
|
||||
<section>
|
||||
<h2>项目信息</h2>
|
||||
<dl class="info-list">
|
||||
<div>
|
||||
<dt>浏览量</dt>
|
||||
<dd>{{ formatViews(project.views) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>更新时间</dt>
|
||||
<dd>{{ project.updateTime }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>功能模块</h2>
|
||||
<div class="pill-list">
|
||||
<span v-for="module in project.modules" :key="module">{{ module }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>适用场景</h2>
|
||||
<div class="pill-list quiet">
|
||||
<span v-for="scene in project.scenes" :key="scene">{{ scene }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-empty v-else-if="!loading" description="未找到源码项目">
|
||||
<el-button type="primary" @click="router.push('/source-store')">返回源码库</el-button>
|
||||
</el-empty>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Back, Download, ShoppingCart } from '@element-plus/icons-vue'
|
||||
import { getSourceDownloadInfo, getSourceProject, purchaseSourceProject } from '@/api/source'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { renderMarkdown } from '@/utils/markdown'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const project = ref(null)
|
||||
const descriptionHtml = computed(() => renderMarkdown(project.value?.descriptionMd || ''))
|
||||
|
||||
onMounted(loadProject)
|
||||
|
||||
async function loadProject() {
|
||||
loading.value = true
|
||||
try {
|
||||
project.value = await getSourceProject(route.params.sourceId)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatViews(views) {
|
||||
return `${Number(views || 0).toLocaleString('zh-CN')} 人浏览`
|
||||
}
|
||||
|
||||
async function handlePurchase() {
|
||||
if (!getToken()) {
|
||||
ElMessage.warning('请先登录后购买源码')
|
||||
router.push({ path: '/login', query: { redirect: route.fullPath } })
|
||||
return
|
||||
}
|
||||
await purchaseSourceProject(project.value.projectId)
|
||||
ElMessage.success('购买成功,源码资源已加入你的账户')
|
||||
await loadProject()
|
||||
}
|
||||
|
||||
async function handleDownload() {
|
||||
if (!getToken()) {
|
||||
ElMessage.warning('请先登录后下载源码')
|
||||
router.push({ path: '/login', query: { redirect: route.fullPath } })
|
||||
return
|
||||
}
|
||||
const info = await getSourceDownloadInfo(project.value.projectId)
|
||||
if (info?.resourceUrl) {
|
||||
window.open(info.resourceUrl, '_blank')
|
||||
return
|
||||
}
|
||||
ElMessage.warning('源码资源暂未配置')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.source-detail-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.detail-hero,
|
||||
.markdown-panel,
|
||||
.summary-panel {
|
||||
border: 1px solid #d9e0ea;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.detail-hero {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.hero-main {
|
||||
display: grid;
|
||||
grid-template-columns: 156px minmax(0, 1fr);
|
||||
gap: 22px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.hero-cover {
|
||||
display: grid;
|
||||
min-height: 156px;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
color: #1e3a8a;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(37, 99, 235, 0.18), rgba(20, 184, 166, 0.16)),
|
||||
#f8fafc;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 6px;
|
||||
color: #1d4ed8;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: #101828;
|
||||
font-size: 30px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.muted {
|
||||
max-width: 760px;
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
|
||||
strong {
|
||||
color: #dc2626;
|
||||
font-size: 22px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-row,
|
||||
.hero-actions,
|
||||
.pill-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.markdown-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 18px 22px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0;
|
||||
color: #101828;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
padding: 24px;
|
||||
color: #344054;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h1),
|
||||
.markdown-body :deep(h2),
|
||||
.markdown-body :deep(h3),
|
||||
.markdown-body :deep(h4) {
|
||||
margin: 24px 0 12px;
|
||||
color: #101828;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h1:first-child),
|
||||
.markdown-body :deep(h2:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h1) {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h2) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(p),
|
||||
.markdown-body :deep(ul),
|
||||
.markdown-body :deep(blockquote),
|
||||
.markdown-body :deep(table),
|
||||
.markdown-body :deep(pre) {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(ul) {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(pre) {
|
||||
overflow: auto;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
color: #dbeafe;
|
||||
background: #172033;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(code) {
|
||||
border-radius: 6px;
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(pre code) {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.markdown-body :deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.markdown-body :deep(th),
|
||||
.markdown-body :deep(td) {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e4e7ed;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-body :deep(blockquote) {
|
||||
padding: 10px 14px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
color: #475467;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.summary-panel {
|
||||
display: grid;
|
||||
gap: 22px;
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 12px;
|
||||
color: #101828;
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-list {
|
||||
margin: 0;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
div:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
dt,
|
||||
dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
dd {
|
||||
color: #101828;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.pill-list span {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pill-list.quiet span {
|
||||
color: #344054;
|
||||
background: #f2f4f7;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.hero-main,
|
||||
.detail-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-cover {
|
||||
min-height: 132px;
|
||||
}
|
||||
|
||||
.hero-title-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
345
RuoYi-Vue/easycode-web/src/views/SourceStoreView.vue
Normal file
345
RuoYi-Vue/easycode-web/src/views/SourceStoreView.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<section class="page source-store-page">
|
||||
<div class="store-hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">源码库 / Source Store</p>
|
||||
<h1>精选 Java 源码项目库</h1>
|
||||
<p class="muted">
|
||||
沉淀可交付、可运行、可二开的企业级源码项目,支持按行业场景和技术栈快速筛选。
|
||||
</p>
|
||||
</div>
|
||||
<div class="store-search">
|
||||
<el-input v-model="keyword" size="large" placeholder="搜索源码项目,例如 DMS、商城、OA">
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="store-body">
|
||||
<aside class="category-panel">
|
||||
<h2>源码分类</h2>
|
||||
<button
|
||||
v-for="category in categories"
|
||||
:key="category.value"
|
||||
class="category-button"
|
||||
:class="{ active: activeCategory === category.value }"
|
||||
type="button"
|
||||
@click="activeCategory = category.value"
|
||||
>
|
||||
{{ category.label }}
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<div class="project-area" v-loading="loading">
|
||||
<div class="project-toolbar">
|
||||
<div>
|
||||
<h2>源码项目</h2>
|
||||
<p class="muted">共 {{ filteredProjects.length }} 个可选项目</p>
|
||||
</div>
|
||||
<el-button :icon="RefreshRight" @click="resetFilters">重置筛选</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredProjects.length" class="source-grid">
|
||||
<article v-for="project in filteredProjects" :key="project.id" class="source-card">
|
||||
<div class="source-cover">
|
||||
<span>{{ project.coverText }}</span>
|
||||
</div>
|
||||
<div class="source-content">
|
||||
<div class="source-heading">
|
||||
<div>
|
||||
<h3>{{ project.name }}</h3>
|
||||
<!-- <p>{{ project.summary }}</p> -->
|
||||
</div>
|
||||
<strong>{{ project.price }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="source-tags">
|
||||
<el-tag v-for="tag in project.tags" :key="tag" effect="plain">{{ tag }}</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="source-meta">
|
||||
<span>{{ formatViews(project.views) }} 人浏览</span>
|
||||
<span>{{ project.updateTime }}</span>
|
||||
</div>
|
||||
|
||||
<div class="source-actions">
|
||||
<el-button :icon="ArrowRight" type="primary" @click="router.push(`/source-store/${project.id}`)">
|
||||
查看详情
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!loading" description="暂无匹配的源码项目">
|
||||
<el-button type="primary" @click="resetFilters">重置筛选</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowRight, RefreshRight, Search } from '@element-plus/icons-vue'
|
||||
import { listSourceCategories, listSourceProjects } from '@/api/source'
|
||||
import { buildSourceCategories } from '@/api/sourceAdapter'
|
||||
|
||||
const router = useRouter()
|
||||
const keyword = ref('')
|
||||
const activeCategory = ref('all')
|
||||
const loading = ref(false)
|
||||
const sourceProjects = ref([])
|
||||
const sourceCategories = ref([])
|
||||
|
||||
const categories = computed(() => buildSourceCategories(sourceProjects.value, sourceCategories.value))
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
const normalizedKeyword = keyword.value.trim().toLowerCase()
|
||||
return sourceProjects.value.filter((project) => {
|
||||
const categoryMatched = activeCategory.value === 'all' || project.category === activeCategory.value
|
||||
if (!categoryMatched) return false
|
||||
if (!normalizedKeyword) return true
|
||||
|
||||
const searchText = [
|
||||
project.name,
|
||||
project.summary,
|
||||
...project.tags,
|
||||
...project.modules,
|
||||
...project.scenes
|
||||
].join(' ').toLowerCase()
|
||||
return searchText.includes(normalizedKeyword)
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(loadProjects)
|
||||
|
||||
async function loadProjects() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [projects, categories] = await Promise.all([
|
||||
listSourceProjects(),
|
||||
listSourceCategories().catch(() => [])
|
||||
])
|
||||
sourceProjects.value = projects
|
||||
sourceCategories.value = categories
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatViews(views) {
|
||||
return Number(views || 0).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
keyword.value = ''
|
||||
activeCategory.value = 'all'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.source-store-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.store-hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 420px);
|
||||
align-items: end;
|
||||
gap: 24px;
|
||||
padding: 28px;
|
||||
border: 1px solid #d9e0ea;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
color: #1d4ed8;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: #101828;
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.muted {
|
||||
max-width: 620px;
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.store-body {
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.category-panel {
|
||||
align-self: start;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 18px;
|
||||
border: 1px solid #d9e0ea;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px;
|
||||
color: #172033;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-button {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
color: #475467;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-button:hover,
|
||||
.category-button.active {
|
||||
border-color: #bfdbfe;
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.project-area {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.project-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid #d9e0ea;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #172033;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.source-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.source-card {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
min-height: 360px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #d9e0ea;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.source-cover {
|
||||
display: grid;
|
||||
min-height: 142px;
|
||||
place-items: center;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(37, 99, 235, 0.16), rgba(20, 184, 166, 0.14)),
|
||||
#f8fafc;
|
||||
|
||||
span {
|
||||
display: grid;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(37, 99, 235, 0.2);
|
||||
border-radius: 8px;
|
||||
color: #1e3a8a;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.source-content {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.source-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #101828;
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #dc2626;
|
||||
font-size: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.source-tags,
|
||||
.source-meta,
|
||||
.source-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.source-meta {
|
||||
justify-content: space-between;
|
||||
color: #475467;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.source-actions {
|
||||
justify-content: flex-end;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.store-hero,
|
||||
.store-body,
|
||||
.source-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
RuoYi-Vue/easycode-web/src/views/sourceStoreLayout.test.mjs
Normal file
56
RuoYi-Vue/easycode-web/src/views/sourceStoreLayout.test.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
||||
const currentDir = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function readView(name) {
|
||||
return readFileSync(resolve(currentDir, name), 'utf8')
|
||||
}
|
||||
|
||||
test('source store cards use vertical cover-first layout', () => {
|
||||
const source = readView('SourceStoreView.vue')
|
||||
|
||||
assert.match(source, /\.source-card\s*\{[\s\S]*grid-template-rows:\s*auto minmax\(0,\s*1fr\);/)
|
||||
assert.doesNotMatch(source, /\.source-card\s*\{[\s\S]*grid-template-columns:\s*112px minmax\(0,\s*1fr\);/)
|
||||
})
|
||||
|
||||
test('source store cards do not actively render project summary', () => {
|
||||
const source = readView('SourceStoreView.vue')
|
||||
const template = source.match(/<template>[\s\S]*<\/template>/)[0]
|
||||
const activeTemplate = template.replace(/<!--[\s\S]*?-->/g, '')
|
||||
|
||||
assert.equal(activeTemplate.includes('project.summary'), false)
|
||||
assert.equal(source.includes('<!-- <p>{{ project.summary }}</p> -->'), true)
|
||||
})
|
||||
|
||||
test('source store loads categories from the front source API', () => {
|
||||
const source = readView('SourceStoreView.vue')
|
||||
|
||||
assert.equal(source.includes('listSourceCategories'), true)
|
||||
assert.match(source, /buildSourceCategories\(sourceProjects\.value,\s*sourceCategories\.value\)/)
|
||||
})
|
||||
|
||||
test('source store and detail do not expose online preview actions', () => {
|
||||
const combinedSource = `${readView('SourceStoreView.vue')}\n${readView('SourceDetailView.vue')}`
|
||||
|
||||
assert.equal(combinedSource.includes('在线预览'), false)
|
||||
assert.equal(combinedSource.includes('handlePreview'), false)
|
||||
})
|
||||
|
||||
test('source detail does not show source size information', () => {
|
||||
const source = readView('SourceDetailView.vue')
|
||||
|
||||
assert.equal(source.includes('源码大小'), false)
|
||||
assert.equal(source.includes('sourceSize'), false)
|
||||
})
|
||||
|
||||
test('source detail keeps purchase as the unauthenticated primary action', () => {
|
||||
const source = readView('SourceDetailView.vue')
|
||||
|
||||
assert.equal(source.includes('购买源码'), true)
|
||||
assert.equal(source.includes('handlePurchase'), true)
|
||||
assert.equal(source.includes('project.purchased ? handleDownload() : handlePurchase()'), true)
|
||||
})
|
||||
@@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||
import com.ruoyi.generator.domain.front.dto.FrontProjectCreateRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.FrontProjectUpdateRequest;
|
||||
@@ -117,10 +118,12 @@ public class FrontProjectController extends BaseController
|
||||
}
|
||||
|
||||
@GetMapping("/{projectId}/download")
|
||||
public void download(@PathVariable Long projectId, @RequestParam String templateType, HttpServletResponse response) throws IOException
|
||||
public void download(@PathVariable Long projectId, @RequestParam(required = false) String templateType, HttpServletResponse response) throws IOException
|
||||
{
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
byte[] data = frontProjectPreviewService.download(userId, projectId, templateType);
|
||||
byte[] data = StringUtils.isNotEmpty(templateType)
|
||||
? frontProjectPreviewService.download(userId, projectId, templateType)
|
||||
: frontProjectPreviewService.downloadAll(userId, projectId);
|
||||
response.reset();
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"project.zip\"");
|
||||
response.addHeader("Content-Length", "" + data.length);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.ruoyi.web.controller.front;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.entity.SysDictData;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.generator.domain.SourceProject;
|
||||
import com.ruoyi.generator.service.ISourceProjectService;
|
||||
import com.ruoyi.system.service.ISysDictTypeService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/front/source")
|
||||
public class FrontSourceController extends BaseController
|
||||
{
|
||||
private static final String SOURCE_PROJECT_CATEGORY_DICT_TYPE = "source_project_category";
|
||||
|
||||
@Autowired
|
||||
private ISourceProjectService sourceProjectService;
|
||||
|
||||
@Autowired
|
||||
private ISysDictTypeService dictTypeService;
|
||||
|
||||
@GetMapping("/categories")
|
||||
public AjaxResult categories()
|
||||
{
|
||||
List<SysDictData> data = dictTypeService.selectDictDataByType(SOURCE_PROJECT_CATEGORY_DICT_TYPE);
|
||||
if (data == null)
|
||||
{
|
||||
data = new ArrayList<SysDictData>();
|
||||
}
|
||||
return success(data);
|
||||
}
|
||||
|
||||
@GetMapping("/projects")
|
||||
public AjaxResult projects(SourceProject query)
|
||||
{
|
||||
return success(sourceProjectService.listPublicProjects(optionalUserId(), query));
|
||||
}
|
||||
|
||||
@GetMapping("/projects/{slug}")
|
||||
public AjaxResult detail(@PathVariable String slug)
|
||||
{
|
||||
return success(sourceProjectService.getPublicProject(optionalUserId(), slug, true));
|
||||
}
|
||||
|
||||
@PostMapping("/projects/{projectId}/purchase")
|
||||
public AjaxResult purchase(@PathVariable Long projectId)
|
||||
{
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
return success(sourceProjectService.purchase(userId, projectId));
|
||||
}
|
||||
|
||||
@GetMapping("/purchases")
|
||||
public AjaxResult purchases()
|
||||
{
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
return success(sourceProjectService.listPurchases(userId));
|
||||
}
|
||||
|
||||
@GetMapping("/projects/{projectId}/download")
|
||||
public AjaxResult download(@PathVariable Long projectId)
|
||||
{
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
return success(sourceProjectService.getDownloadInfo(userId, projectId));
|
||||
}
|
||||
|
||||
private Long optionalUserId()
|
||||
{
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null || authentication.getPrincipal() == null
|
||||
|| "anonymousUser".equals(authentication.getPrincipal()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (authentication.getPrincipal() instanceof LoginUser)
|
||||
{
|
||||
return ((LoginUser) authentication.getPrincipal()).getUserId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,13 @@ ruoyi:
|
||||
captchaType: math
|
||||
|
||||
deepseek:
|
||||
apiKey: ${DEEPSEEK_API_KEY:}
|
||||
apiKey: sk-2aef3231d45048a9a0ba6f0367a072ca
|
||||
baseUrl: https://api.deepseek.com
|
||||
model: deepseek-chat
|
||||
timeout: 60000
|
||||
model: deepseek-v4-flash
|
||||
timeout: ${DEEPSEEK_TIMEOUT:180000}
|
||||
maxTables: 12
|
||||
maxColumnsPerTable: 30
|
||||
skipSslVerify: true
|
||||
|
||||
# 开发环境配置
|
||||
server:
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.ruoyi.web.controller.front;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.entity.SysDictData;
|
||||
import com.ruoyi.system.service.ISysDictTypeService;
|
||||
|
||||
public class FrontSourceControllerTest
|
||||
{
|
||||
private FrontSourceController controller;
|
||||
|
||||
@Mock
|
||||
private ISysDictTypeService dictTypeService;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
MockitoAnnotations.initMocks(this);
|
||||
controller = new FrontSourceController();
|
||||
setField("dictTypeService", dictTypeService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void categoriesReturnsSourceProjectCategoryDictData()
|
||||
{
|
||||
SysDictData data = new SysDictData();
|
||||
data.setDictLabel("Enterprise Systems");
|
||||
data.setDictValue("enterprise");
|
||||
data.setDictType("source_project_category");
|
||||
when(dictTypeService.selectDictDataByType("source_project_category")).thenReturn(Collections.singletonList(data));
|
||||
|
||||
AjaxResult result = controller.categories();
|
||||
|
||||
List<?> categories = (List<?>) result.get(AjaxResult.DATA_TAG);
|
||||
SysDictData category = (SysDictData) categories.get(0);
|
||||
assertEquals("enterprise", category.getDictValue());
|
||||
assertEquals("Enterprise Systems", category.getDictLabel());
|
||||
}
|
||||
|
||||
private void setField(String name, Object value) throws Exception
|
||||
{
|
||||
Field field = FrontSourceController.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(controller, value);
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,7 @@ public class SecurityConfig
|
||||
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
|
||||
requests.antMatchers("/login", "/register", "/captchaImage",
|
||||
"/front/auth/login", "/front/auth/register").permitAll()
|
||||
.antMatchers(HttpMethod.GET, "/front/source/categories", "/front/source/projects", "/front/source/projects/*").permitAll()
|
||||
// 静态资源,可匿名访问
|
||||
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
|
||||
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
|
||||
|
||||
@@ -10,9 +10,10 @@ public class DeepSeekProperties
|
||||
private String apiKey;
|
||||
private String baseUrl = "https://api.deepseek.com";
|
||||
private String model = "deepseek-chat";
|
||||
private Integer timeout = 60000;
|
||||
private Integer timeout = 180000;
|
||||
private Integer maxTables = 12;
|
||||
private Integer maxColumnsPerTable = 30;
|
||||
private Boolean skipSslVerify = false;
|
||||
|
||||
public String getApiKey() { return apiKey; }
|
||||
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
|
||||
@@ -26,4 +27,7 @@ public class DeepSeekProperties
|
||||
public void setMaxTables(Integer maxTables) { this.maxTables = maxTables; }
|
||||
public Integer getMaxColumnsPerTable() { return maxColumnsPerTable; }
|
||||
public void setMaxColumnsPerTable(Integer maxColumnsPerTable) { this.maxColumnsPerTable = maxColumnsPerTable; }
|
||||
public Boolean getSkipSslVerify() { return skipSslVerify; }
|
||||
public void setSkipSslVerify(Boolean skipSslVerify) { this.skipSslVerify = skipSslVerify; }
|
||||
public boolean isSkipSslVerify() { return Boolean.TRUE.equals(skipSslVerify); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.ruoyi.generator.controller;
|
||||
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.generator.domain.SourceProject;
|
||||
import com.ruoyi.generator.service.ISourceProjectService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
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.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/generator/sourceProject")
|
||||
public class SourceProjectController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ISourceProjectService sourceProjectService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourceProject:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SourceProject sourceProject)
|
||||
{
|
||||
startPage();
|
||||
List<SourceProject> list = sourceProjectService.selectSourceProjectList(sourceProject);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourceProject:export')")
|
||||
@Log(title = "源码项目", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, SourceProject sourceProject)
|
||||
{
|
||||
List<SourceProject> list = sourceProjectService.selectSourceProjectList(sourceProject);
|
||||
ExcelUtil<SourceProject> util = new ExcelUtil<SourceProject>(SourceProject.class);
|
||||
util.exportExcel(response, list, "源码项目数据");
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourceProject:query')")
|
||||
@GetMapping(value = "/{projectId}")
|
||||
public AjaxResult getInfo(@PathVariable("projectId") Long projectId)
|
||||
{
|
||||
return success(sourceProjectService.selectSourceProjectByProjectId(projectId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourceProject:add')")
|
||||
@Log(title = "源码项目", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody SourceProject sourceProject)
|
||||
{
|
||||
return toAjax(sourceProjectService.insertSourceProject(sourceProject));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourceProject:edit')")
|
||||
@Log(title = "源码项目", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody SourceProject sourceProject)
|
||||
{
|
||||
return toAjax(sourceProjectService.updateSourceProject(sourceProject));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourceProject:edit')")
|
||||
@Log(title = "源码项目状态", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/changeStatus")
|
||||
public AjaxResult changeStatus(@RequestBody SourceProject sourceProject)
|
||||
{
|
||||
return toAjax(sourceProjectService.changeSourceProjectStatus(sourceProject));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourceProject:remove')")
|
||||
@Log(title = "源码项目", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{projectIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] projectIds)
|
||||
{
|
||||
return toAjax(sourceProjectService.deleteSourceProjectByProjectIds(projectIds));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.ruoyi.generator.controller;
|
||||
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.generator.domain.SourcePurchase;
|
||||
import com.ruoyi.generator.service.ISourcePurchaseService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
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.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/generator/sourcePurchase")
|
||||
public class SourcePurchaseController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ISourcePurchaseService sourcePurchaseService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourcePurchase:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SourcePurchase sourcePurchase)
|
||||
{
|
||||
startPage();
|
||||
List<SourcePurchase> list = sourcePurchaseService.selectSourcePurchaseList(sourcePurchase);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourcePurchase:export')")
|
||||
@Log(title = "源码购买记录", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, SourcePurchase sourcePurchase)
|
||||
{
|
||||
List<SourcePurchase> list = sourcePurchaseService.selectSourcePurchaseList(sourcePurchase);
|
||||
ExcelUtil<SourcePurchase> util = new ExcelUtil<SourcePurchase>(SourcePurchase.class);
|
||||
util.exportExcel(response, list, "源码购买记录数据");
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourcePurchase:query')")
|
||||
@GetMapping(value = "/{purchaseId}")
|
||||
public AjaxResult getInfo(@PathVariable("purchaseId") Long purchaseId)
|
||||
{
|
||||
return success(sourcePurchaseService.selectSourcePurchaseByPurchaseId(purchaseId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('generator:sourcePurchase:edit')")
|
||||
@Log(title = "源码购买记录", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody SourcePurchase sourcePurchase)
|
||||
{
|
||||
return toAjax(sourcePurchaseService.updateSourcePurchase(sourcePurchase));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.ruoyi.generator.domain;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
public class SourceProject extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
@Excel(name = "访问标识")
|
||||
private String slug;
|
||||
|
||||
@Excel(name = "项目名称")
|
||||
private String projectName;
|
||||
|
||||
@Excel(name = "分类")
|
||||
private String category;
|
||||
|
||||
@Excel(name = "封面标识")
|
||||
private String coverText;
|
||||
|
||||
@Excel(name = "项目摘要")
|
||||
private String summary;
|
||||
|
||||
private String descriptionMd;
|
||||
|
||||
@Excel(name = "价格")
|
||||
private Long price;
|
||||
|
||||
@Excel(name = "浏览量")
|
||||
private Long viewCount;
|
||||
|
||||
private String tags;
|
||||
private String modules;
|
||||
private String scenes;
|
||||
|
||||
@Excel(name = "资源名称")
|
||||
private String resourceName;
|
||||
|
||||
private String resourceUrl;
|
||||
|
||||
@Excel(name = "资源版本")
|
||||
private String resourceVersion;
|
||||
|
||||
@Excel(name = "资源大小")
|
||||
private String resourceSize;
|
||||
|
||||
@Excel(name = "排序")
|
||||
private Integer sort;
|
||||
|
||||
@Excel(name = "状态", readConverterExp = "0=上架,1=下架")
|
||||
private String status;
|
||||
|
||||
private String delFlag;
|
||||
|
||||
private String keyword;
|
||||
|
||||
public Long getProjectId() { return projectId; }
|
||||
public void setProjectId(Long projectId) { this.projectId = projectId; }
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
public String getProjectName() { return projectName; }
|
||||
public void setProjectName(String projectName) { this.projectName = projectName; }
|
||||
public String getCategory() { return category; }
|
||||
public void setCategory(String category) { this.category = category; }
|
||||
public String getCoverText() { return coverText; }
|
||||
public void setCoverText(String coverText) { this.coverText = coverText; }
|
||||
public String getSummary() { return summary; }
|
||||
public void setSummary(String summary) { this.summary = summary; }
|
||||
public String getDescriptionMd() { return descriptionMd; }
|
||||
public void setDescriptionMd(String descriptionMd) { this.descriptionMd = descriptionMd; }
|
||||
public Long getPrice() { return price; }
|
||||
public void setPrice(Long price) { this.price = price; }
|
||||
public Long getViewCount() { return viewCount; }
|
||||
public void setViewCount(Long viewCount) { this.viewCount = viewCount; }
|
||||
public String getTags() { return tags; }
|
||||
public void setTags(String tags) { this.tags = tags; }
|
||||
public String getModules() { return modules; }
|
||||
public void setModules(String modules) { this.modules = modules; }
|
||||
public String getScenes() { return scenes; }
|
||||
public void setScenes(String scenes) { this.scenes = scenes; }
|
||||
public String getResourceName() { return resourceName; }
|
||||
public void setResourceName(String resourceName) { this.resourceName = resourceName; }
|
||||
public String getResourceUrl() { return resourceUrl; }
|
||||
public void setResourceUrl(String resourceUrl) { this.resourceUrl = resourceUrl; }
|
||||
public String getResourceVersion() { return resourceVersion; }
|
||||
public void setResourceVersion(String resourceVersion) { this.resourceVersion = resourceVersion; }
|
||||
public String getResourceSize() { return resourceSize; }
|
||||
public void setResourceSize(String resourceSize) { this.resourceSize = resourceSize; }
|
||||
public Integer getSort() { return sort; }
|
||||
public void setSort(Integer sort) { this.sort = sort; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getDelFlag() { return delFlag; }
|
||||
public void setDelFlag(String delFlag) { this.delFlag = delFlag; }
|
||||
public String getKeyword() { return keyword; }
|
||||
public void setKeyword(String keyword) { this.keyword = keyword; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.generator.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
public class SourcePurchase extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long purchaseId;
|
||||
private Long projectId;
|
||||
private Long userId;
|
||||
|
||||
@Excel(name = "订单号")
|
||||
private String orderNo;
|
||||
|
||||
@Excel(name = "金额")
|
||||
private Long amount;
|
||||
|
||||
@Excel(name = "支付状态", readConverterExp = "0=待支付,1=已支付,2=已取消")
|
||||
private String payStatus;
|
||||
|
||||
@Excel(name = "资源状态", readConverterExp = "0=未发放,1=已发放")
|
||||
private String resourceStatus;
|
||||
|
||||
@Excel(name = "购买时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date purchaseTime;
|
||||
|
||||
private String projectName;
|
||||
|
||||
public Long getPurchaseId() { return purchaseId; }
|
||||
public void setPurchaseId(Long purchaseId) { this.purchaseId = purchaseId; }
|
||||
public Long getProjectId() { return projectId; }
|
||||
public void setProjectId(Long projectId) { this.projectId = projectId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getOrderNo() { return orderNo; }
|
||||
public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
|
||||
public Long getAmount() { return amount; }
|
||||
public void setAmount(Long amount) { this.amount = amount; }
|
||||
public String getPayStatus() { return payStatus; }
|
||||
public void setPayStatus(String payStatus) { this.payStatus = payStatus; }
|
||||
public String getResourceStatus() { return resourceStatus; }
|
||||
public void setResourceStatus(String resourceStatus) { this.resourceStatus = resourceStatus; }
|
||||
public Date getPurchaseTime() { return purchaseTime; }
|
||||
public void setPurchaseTime(Date purchaseTime) { this.purchaseTime = purchaseTime; }
|
||||
public String getProjectName() { return projectName; }
|
||||
public void setProjectName(String projectName) { this.projectName = projectName; }
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public class FrontProject extends BaseEntity
|
||||
private String backFramework;
|
||||
private String generateStatus;
|
||||
private String previewStatus;
|
||||
private String erDiagram;
|
||||
private String status;
|
||||
private List<FrontProjectTable> tables;
|
||||
|
||||
@@ -46,6 +47,8 @@ public class FrontProject extends BaseEntity
|
||||
public void setGenerateStatus(String generateStatus) { this.generateStatus = generateStatus; }
|
||||
public String getPreviewStatus() { return previewStatus; }
|
||||
public void setPreviewStatus(String previewStatus) { this.previewStatus = previewStatus; }
|
||||
public String getErDiagram() { return erDiagram; }
|
||||
public void setErDiagram(String erDiagram) { this.erDiagram = erDiagram; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public List<FrontProjectTable> getTables() { return tables; }
|
||||
|
||||
@@ -7,6 +7,7 @@ public class DatabaseDesignResponse
|
||||
private Long projectId;
|
||||
private List<DatabaseTableDesign> tables;
|
||||
private String sql;
|
||||
private Object erDiagram;
|
||||
|
||||
public Long getProjectId() { return projectId; }
|
||||
public void setProjectId(Long projectId) { this.projectId = projectId; }
|
||||
@@ -14,4 +15,6 @@ public class DatabaseDesignResponse
|
||||
public void setTables(List<DatabaseTableDesign> tables) { this.tables = tables; }
|
||||
public String getSql() { return sql; }
|
||||
public void setSql(String sql) { this.sql = sql; }
|
||||
public Object getErDiagram() { return erDiagram; }
|
||||
public void setErDiagram(Object erDiagram) { this.erDiagram = erDiagram; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ruoyi.generator.domain.source;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class SourceProjectResponse
|
||||
{
|
||||
private Long projectId;
|
||||
private String slug;
|
||||
private String projectName;
|
||||
private String category;
|
||||
private String coverText;
|
||||
private String summary;
|
||||
private String descriptionMd;
|
||||
private Long price;
|
||||
private String priceLabel;
|
||||
private Long viewCount;
|
||||
private Date updateTime;
|
||||
private List<String> tags;
|
||||
private List<String> modules;
|
||||
private List<String> scenes;
|
||||
private boolean purchased;
|
||||
private boolean resourceReady;
|
||||
|
||||
public Long getProjectId() { return projectId; }
|
||||
public void setProjectId(Long projectId) { this.projectId = projectId; }
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
public String getProjectName() { return projectName; }
|
||||
public void setProjectName(String projectName) { this.projectName = projectName; }
|
||||
public String getCategory() { return category; }
|
||||
public void setCategory(String category) { this.category = category; }
|
||||
public String getCoverText() { return coverText; }
|
||||
public void setCoverText(String coverText) { this.coverText = coverText; }
|
||||
public String getSummary() { return summary; }
|
||||
public void setSummary(String summary) { this.summary = summary; }
|
||||
public String getDescriptionMd() { return descriptionMd; }
|
||||
public void setDescriptionMd(String descriptionMd) { this.descriptionMd = descriptionMd; }
|
||||
public Long getPrice() { return price; }
|
||||
public void setPrice(Long price) { this.price = price; }
|
||||
public String getPriceLabel() { return priceLabel; }
|
||||
public void setPriceLabel(String priceLabel) { this.priceLabel = priceLabel; }
|
||||
public Long getViewCount() { return viewCount; }
|
||||
public void setViewCount(Long viewCount) { this.viewCount = viewCount; }
|
||||
public Date getUpdateTime() { return updateTime; }
|
||||
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
|
||||
public List<String> getTags() { return tags; }
|
||||
public void setTags(List<String> tags) { this.tags = tags; }
|
||||
public List<String> getModules() { return modules; }
|
||||
public void setModules(List<String> modules) { this.modules = modules; }
|
||||
public List<String> getScenes() { return scenes; }
|
||||
public void setScenes(List<String> scenes) { this.scenes = scenes; }
|
||||
public boolean isPurchased() { return purchased; }
|
||||
public void setPurchased(boolean purchased) { this.purchased = purchased; }
|
||||
public boolean isResourceReady() { return resourceReady; }
|
||||
public void setResourceReady(boolean resourceReady) { this.resourceReady = resourceReady; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ruoyi.generator.domain.source;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class SourcePurchaseResponse
|
||||
{
|
||||
private Long purchaseId;
|
||||
private Long projectId;
|
||||
private String projectName;
|
||||
private String orderNo;
|
||||
private Long amount;
|
||||
private String amountLabel;
|
||||
private String payStatus;
|
||||
private String resourceStatus;
|
||||
private Date purchaseTime;
|
||||
|
||||
public Long getPurchaseId() { return purchaseId; }
|
||||
public void setPurchaseId(Long purchaseId) { this.purchaseId = purchaseId; }
|
||||
public Long getProjectId() { return projectId; }
|
||||
public void setProjectId(Long projectId) { this.projectId = projectId; }
|
||||
public String getProjectName() { return projectName; }
|
||||
public void setProjectName(String projectName) { this.projectName = projectName; }
|
||||
public String getOrderNo() { return orderNo; }
|
||||
public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
|
||||
public Long getAmount() { return amount; }
|
||||
public void setAmount(Long amount) { this.amount = amount; }
|
||||
public String getAmountLabel() { return amountLabel; }
|
||||
public void setAmountLabel(String amountLabel) { this.amountLabel = amountLabel; }
|
||||
public String getPayStatus() { return payStatus; }
|
||||
public void setPayStatus(String payStatus) { this.payStatus = payStatus; }
|
||||
public String getResourceStatus() { return resourceStatus; }
|
||||
public void setResourceStatus(String resourceStatus) { this.resourceStatus = resourceStatus; }
|
||||
public Date getPurchaseTime() { return purchaseTime; }
|
||||
public void setPurchaseTime(Date purchaseTime) { this.purchaseTime = purchaseTime; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ruoyi.generator.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.generator.domain.SourceProject;
|
||||
|
||||
public interface SourceProjectMapper
|
||||
{
|
||||
public SourceProject selectSourceProjectByProjectId(Long projectId);
|
||||
|
||||
public SourceProject selectSourceProjectBySlug(String slug);
|
||||
|
||||
public List<SourceProject> selectSourceProjectList(SourceProject sourceProject);
|
||||
|
||||
public List<SourceProject> selectPublicSourceProjectList(SourceProject sourceProject);
|
||||
|
||||
public int insertSourceProject(SourceProject sourceProject);
|
||||
|
||||
public int updateSourceProject(SourceProject sourceProject);
|
||||
|
||||
public int deleteSourceProjectByProjectIds(Long[] projectIds);
|
||||
|
||||
public int changeSourceProjectStatus(SourceProject sourceProject);
|
||||
|
||||
public int increaseViewCount(Long projectId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.generator.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.generator.domain.SourcePurchase;
|
||||
|
||||
public interface SourcePurchaseMapper
|
||||
{
|
||||
public SourcePurchase selectSourcePurchaseByPurchaseId(Long purchaseId);
|
||||
|
||||
public SourcePurchase selectPaidPurchase(@Param("userId") Long userId, @Param("projectId") Long projectId);
|
||||
|
||||
public List<SourcePurchase> selectSourcePurchaseList(SourcePurchase sourcePurchase);
|
||||
|
||||
public List<SourcePurchase> selectSourcePurchasesByUserId(Long userId);
|
||||
|
||||
public int insertSourcePurchase(SourcePurchase sourcePurchase);
|
||||
|
||||
public int updateSourcePurchase(SourcePurchase sourcePurchase);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ruoyi.generator.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.generator.domain.SourceProject;
|
||||
import com.ruoyi.generator.domain.SourcePurchase;
|
||||
import com.ruoyi.generator.domain.source.SourceProjectResponse;
|
||||
import com.ruoyi.generator.domain.source.SourcePurchaseResponse;
|
||||
|
||||
public interface ISourceProjectService
|
||||
{
|
||||
public SourceProject selectSourceProjectByProjectId(Long projectId);
|
||||
|
||||
public List<SourceProject> selectSourceProjectList(SourceProject sourceProject);
|
||||
|
||||
public int insertSourceProject(SourceProject sourceProject);
|
||||
|
||||
public int updateSourceProject(SourceProject sourceProject);
|
||||
|
||||
public int deleteSourceProjectByProjectIds(Long[] projectIds);
|
||||
|
||||
public int changeSourceProjectStatus(SourceProject sourceProject);
|
||||
|
||||
public List<SourceProjectResponse> listPublicProjects(Long userId, SourceProject query);
|
||||
|
||||
public SourceProjectResponse getPublicProject(Long userId, String slug, boolean increaseView);
|
||||
|
||||
public SourcePurchase purchase(Long userId, Long projectId);
|
||||
|
||||
public List<SourcePurchaseResponse> listPurchases(Long userId);
|
||||
|
||||
public Map<String, Object> getDownloadInfo(Long userId, Long projectId);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ruoyi.generator.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.generator.domain.SourcePurchase;
|
||||
|
||||
public interface ISourcePurchaseService
|
||||
{
|
||||
public SourcePurchase selectSourcePurchaseByPurchaseId(Long purchaseId);
|
||||
|
||||
public List<SourcePurchase> selectSourcePurchaseList(SourcePurchase sourcePurchase);
|
||||
|
||||
public int updateSourcePurchase(SourcePurchase sourcePurchase);
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package com.ruoyi.generator.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.domain.SourceProject;
|
||||
import com.ruoyi.generator.domain.SourcePurchase;
|
||||
import com.ruoyi.generator.domain.source.SourceProjectResponse;
|
||||
import com.ruoyi.generator.domain.source.SourcePurchaseResponse;
|
||||
import com.ruoyi.generator.mapper.SourceProjectMapper;
|
||||
import com.ruoyi.generator.mapper.SourcePurchaseMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class SourceProjectServiceImpl implements ISourceProjectService
|
||||
{
|
||||
private static final Pattern SLUG_PATTERN = Pattern.compile("^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$");
|
||||
|
||||
@Autowired
|
||||
private SourceProjectMapper sourceProjectMapper;
|
||||
|
||||
@Autowired
|
||||
private SourcePurchaseMapper sourcePurchaseMapper;
|
||||
|
||||
@Override
|
||||
public SourceProject selectSourceProjectByProjectId(Long projectId)
|
||||
{
|
||||
return sourceProjectMapper.selectSourceProjectByProjectId(projectId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SourceProject> selectSourceProjectList(SourceProject sourceProject)
|
||||
{
|
||||
return sourceProjectMapper.selectSourceProjectList(sourceProject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertSourceProject(SourceProject sourceProject)
|
||||
{
|
||||
prepareForSave(sourceProject, true);
|
||||
sourceProject.setCreateTime(DateUtils.getNowDate());
|
||||
return sourceProjectMapper.insertSourceProject(sourceProject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateSourceProject(SourceProject sourceProject)
|
||||
{
|
||||
prepareForSave(sourceProject, false);
|
||||
sourceProject.setUpdateTime(DateUtils.getNowDate());
|
||||
return sourceProjectMapper.updateSourceProject(sourceProject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteSourceProjectByProjectIds(Long[] projectIds)
|
||||
{
|
||||
return sourceProjectMapper.deleteSourceProjectByProjectIds(projectIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int changeSourceProjectStatus(SourceProject sourceProject)
|
||||
{
|
||||
sourceProject.setUpdateTime(DateUtils.getNowDate());
|
||||
return sourceProjectMapper.changeSourceProjectStatus(sourceProject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SourceProjectResponse> listPublicProjects(Long userId, SourceProject query)
|
||||
{
|
||||
List<SourceProjectResponse> responses = new ArrayList<SourceProjectResponse>();
|
||||
List<SourceProject> projects = sourceProjectMapper.selectPublicSourceProjectList(query);
|
||||
for (SourceProject project : projects)
|
||||
{
|
||||
responses.add(toProjectResponse(project, hasPaid(userId, project.getProjectId())));
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceProjectResponse getPublicProject(Long userId, String slug, boolean increaseView)
|
||||
{
|
||||
SourceProject project = sourceProjectMapper.selectSourceProjectBySlug(slug);
|
||||
assertProjectPublished(project);
|
||||
if (increaseView)
|
||||
{
|
||||
sourceProjectMapper.increaseViewCount(project.getProjectId());
|
||||
project.setViewCount(Long.valueOf(defaultLong(project.getViewCount()) + 1L));
|
||||
}
|
||||
return toProjectResponse(project, hasPaid(userId, project.getProjectId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourcePurchase purchase(Long userId, Long projectId)
|
||||
{
|
||||
assertFrontUserLoggedIn(userId);
|
||||
SourceProject project = sourceProjectMapper.selectSourceProjectByProjectId(projectId);
|
||||
assertProjectPublished(project);
|
||||
SourcePurchase paid = sourcePurchaseMapper.selectPaidPurchase(userId, projectId);
|
||||
if (paid != null)
|
||||
{
|
||||
return paid;
|
||||
}
|
||||
SourcePurchase purchase = new SourcePurchase();
|
||||
purchase.setProjectId(projectId);
|
||||
purchase.setUserId(userId);
|
||||
purchase.setOrderNo(buildOrderNo(userId));
|
||||
purchase.setAmount(project.getPrice());
|
||||
purchase.setPayStatus("1");
|
||||
purchase.setResourceStatus(StringUtils.isNotEmpty(project.getResourceUrl()) ? "1" : "0");
|
||||
purchase.setPurchaseTime(DateUtils.getNowDate());
|
||||
purchase.setCreateTime(DateUtils.getNowDate());
|
||||
sourcePurchaseMapper.insertSourcePurchase(purchase);
|
||||
return purchase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SourcePurchaseResponse> listPurchases(Long userId)
|
||||
{
|
||||
assertFrontUserLoggedIn(userId);
|
||||
List<SourcePurchaseResponse> responses = new ArrayList<SourcePurchaseResponse>();
|
||||
List<SourcePurchase> purchases = sourcePurchaseMapper.selectSourcePurchasesByUserId(userId);
|
||||
for (SourcePurchase purchase : purchases)
|
||||
{
|
||||
responses.add(toPurchaseResponse(purchase));
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDownloadInfo(Long userId, Long projectId)
|
||||
{
|
||||
assertFrontUserLoggedIn(userId);
|
||||
SourceProject project = sourceProjectMapper.selectSourceProjectByProjectId(projectId);
|
||||
assertProjectPublished(project);
|
||||
SourcePurchase paid = sourcePurchaseMapper.selectPaidPurchase(userId, projectId);
|
||||
if (paid == null)
|
||||
{
|
||||
throw new ServiceException("请先购买源码");
|
||||
}
|
||||
if (StringUtils.isEmpty(project.getResourceUrl()))
|
||||
{
|
||||
throw new ServiceException("源码资源暂未配置");
|
||||
}
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
result.put("projectId", project.getProjectId());
|
||||
result.put("projectName", project.getProjectName());
|
||||
result.put("resourceName", project.getResourceName());
|
||||
result.put("resourceUrl", project.getResourceUrl());
|
||||
result.put("resourceVersion", project.getResourceVersion());
|
||||
return result;
|
||||
}
|
||||
|
||||
private SourceProjectResponse toProjectResponse(SourceProject project, boolean purchased)
|
||||
{
|
||||
SourceProjectResponse response = new SourceProjectResponse();
|
||||
response.setProjectId(project.getProjectId());
|
||||
response.setSlug(project.getSlug());
|
||||
response.setProjectName(project.getProjectName());
|
||||
response.setCategory(project.getCategory());
|
||||
response.setCoverText(project.getCoverText());
|
||||
response.setSummary(project.getSummary());
|
||||
response.setDescriptionMd(project.getDescriptionMd());
|
||||
response.setPrice(project.getPrice());
|
||||
response.setPriceLabel(formatPrice(project.getPrice()));
|
||||
response.setViewCount(Long.valueOf(defaultLong(project.getViewCount())));
|
||||
response.setUpdateTime(project.getUpdateTime() == null ? project.getCreateTime() : project.getUpdateTime());
|
||||
response.setTags(readStringList(project.getTags()));
|
||||
response.setModules(readStringList(project.getModules()));
|
||||
response.setScenes(readStringList(project.getScenes()));
|
||||
response.setPurchased(purchased);
|
||||
response.setResourceReady(StringUtils.isNotEmpty(project.getResourceUrl()));
|
||||
return response;
|
||||
}
|
||||
|
||||
private SourcePurchaseResponse toPurchaseResponse(SourcePurchase purchase)
|
||||
{
|
||||
SourcePurchaseResponse response = new SourcePurchaseResponse();
|
||||
response.setPurchaseId(purchase.getPurchaseId());
|
||||
response.setProjectId(purchase.getProjectId());
|
||||
response.setProjectName(purchase.getProjectName());
|
||||
response.setOrderNo(purchase.getOrderNo());
|
||||
response.setAmount(purchase.getAmount());
|
||||
response.setAmountLabel(formatPrice(purchase.getAmount()));
|
||||
response.setPayStatus(purchase.getPayStatus());
|
||||
response.setResourceStatus(purchase.getResourceStatus());
|
||||
response.setPurchaseTime(purchase.getPurchaseTime());
|
||||
return response;
|
||||
}
|
||||
|
||||
private boolean hasPaid(Long userId, Long projectId)
|
||||
{
|
||||
return userId != null && sourcePurchaseMapper.selectPaidPurchase(userId, projectId) != null;
|
||||
}
|
||||
|
||||
private void assertFrontUserLoggedIn(Long userId)
|
||||
{
|
||||
if (userId == null)
|
||||
{
|
||||
throw new ServiceException("前台用户未登录");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertProjectPublished(SourceProject project)
|
||||
{
|
||||
if (project == null || !"0".equals(project.getStatus()) || "2".equals(project.getDelFlag()))
|
||||
{
|
||||
throw new ServiceException("源码项目未上架或不存在");
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareForSave(SourceProject project, boolean inserting)
|
||||
{
|
||||
if (project == null || StringUtils.isEmpty(project.getProjectName()))
|
||||
{
|
||||
throw new ServiceException("项目名称不能为空");
|
||||
}
|
||||
if (StringUtils.isEmpty(project.getSlug()))
|
||||
{
|
||||
project.setSlug(toSlug(project.getProjectName()));
|
||||
}
|
||||
if (StringUtils.isEmpty(project.getCoverText()))
|
||||
{
|
||||
project.setCoverText(project.getSlug().toUpperCase(Locale.ENGLISH));
|
||||
}
|
||||
project.setTags(normalizeJsonList(project.getTags()));
|
||||
project.setModules(normalizeJsonList(project.getModules()));
|
||||
project.setScenes(normalizeJsonList(project.getScenes()));
|
||||
if (project.getPrice() == null)
|
||||
{
|
||||
project.setPrice(0L);
|
||||
}
|
||||
if (project.getViewCount() == null)
|
||||
{
|
||||
project.setViewCount(0L);
|
||||
}
|
||||
if (project.getSort() == null)
|
||||
{
|
||||
project.setSort(0);
|
||||
}
|
||||
if (StringUtils.isEmpty(project.getStatus()))
|
||||
{
|
||||
project.setStatus("0");
|
||||
}
|
||||
if (inserting && StringUtils.isEmpty(project.getDelFlag()))
|
||||
{
|
||||
project.setDelFlag("0");
|
||||
}
|
||||
}
|
||||
|
||||
private String toSlug(String value)
|
||||
{
|
||||
String slug = StringUtils.defaultString(value).trim()
|
||||
.replaceAll("[^A-Za-z0-9]+", "-")
|
||||
.replaceAll("^-+|-+$", "")
|
||||
.replaceAll("-{2,}", "-")
|
||||
.toLowerCase(Locale.ENGLISH);
|
||||
if (!SLUG_PATTERN.matcher(slug).matches())
|
||||
{
|
||||
return "source-" + System.currentTimeMillis();
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
|
||||
private List<String> readStringList(String value)
|
||||
{
|
||||
if (StringUtils.isEmpty(value))
|
||||
{
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
try
|
||||
{
|
||||
List<String> list = JSON.parseArray(value, String.class);
|
||||
return list == null ? new ArrayList<String>() : list;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return splitList(value);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeJsonList(String value)
|
||||
{
|
||||
return JSON.toJSONString(readStringList(value));
|
||||
}
|
||||
|
||||
private List<String> splitList(String value)
|
||||
{
|
||||
List<String> list = new ArrayList<String>();
|
||||
if (StringUtils.isEmpty(value))
|
||||
{
|
||||
return list;
|
||||
}
|
||||
String[] parts = value.split("[,,\\n]");
|
||||
for (String part : parts)
|
||||
{
|
||||
String item = part == null ? "" : part.trim();
|
||||
if (StringUtils.isNotEmpty(item))
|
||||
{
|
||||
list.add(item);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String formatPrice(Long cents)
|
||||
{
|
||||
BigDecimal amount = BigDecimal.valueOf(defaultLong(cents), 2).stripTrailingZeros();
|
||||
return "¥" + amount.toPlainString();
|
||||
}
|
||||
|
||||
private long defaultLong(Long value)
|
||||
{
|
||||
return value == null ? 0L : value.longValue();
|
||||
}
|
||||
|
||||
private String buildOrderNo(Long userId)
|
||||
{
|
||||
String time = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
|
||||
String random = UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase(Locale.ENGLISH);
|
||||
return "SRC" + time + userId + random;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ruoyi.generator.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.generator.domain.SourcePurchase;
|
||||
import com.ruoyi.generator.mapper.SourcePurchaseMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class SourcePurchaseServiceImpl implements ISourcePurchaseService
|
||||
{
|
||||
@Autowired
|
||||
private SourcePurchaseMapper sourcePurchaseMapper;
|
||||
|
||||
@Override
|
||||
public SourcePurchase selectSourcePurchaseByPurchaseId(Long purchaseId)
|
||||
{
|
||||
return sourcePurchaseMapper.selectSourcePurchaseByPurchaseId(purchaseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SourcePurchase> selectSourcePurchaseList(SourcePurchase sourcePurchase)
|
||||
{
|
||||
return sourcePurchaseMapper.selectSourcePurchaseList(sourcePurchase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateSourcePurchase(SourcePurchase sourcePurchase)
|
||||
{
|
||||
sourcePurchase.setUpdateTime(DateUtils.getNowDate());
|
||||
return sourcePurchaseMapper.updateSourcePurchase(sourcePurchase);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.config.DeepSeekProperties;
|
||||
@@ -61,6 +65,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
normalizeAndBuildSql(response);
|
||||
validator.validate(response, maxTables(), maxColumnsPerTable());
|
||||
persistDatabase(userId, projectId, response);
|
||||
markDatabaseGenerated(projectId);
|
||||
generation.setResponsePayload(aiContent);
|
||||
generation.setSuccess("1");
|
||||
generation.setElapsedMs(System.currentTimeMillis() - start);
|
||||
@@ -116,7 +121,13 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
private DatabaseDesignResponse parseResponse(String aiContent)
|
||||
{
|
||||
String content = stripMarkdownFence(aiContent);
|
||||
DatabaseDesignResponse response = JSON.parseObject(content, DatabaseDesignResponse.class);
|
||||
JSONObject json = JSON.parseObject(content);
|
||||
if (json == null)
|
||||
{
|
||||
throw new ServiceException("AI database design is empty");
|
||||
}
|
||||
normalizeResponseAliases(json);
|
||||
DatabaseDesignResponse response = json.toJavaObject(DatabaseDesignResponse.class);
|
||||
if (response == null)
|
||||
{
|
||||
throw new ServiceException("AI 数据库设计为空");
|
||||
@@ -124,6 +135,90 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
return response;
|
||||
}
|
||||
|
||||
private void normalizeResponseAliases(JSONObject response)
|
||||
{
|
||||
if (response == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
JSONArray tables = response.getJSONArray("tables");
|
||||
if (tables == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (int tableIndex = 0; tableIndex < tables.size(); tableIndex++)
|
||||
{
|
||||
JSONObject table = tables.getJSONObject(tableIndex);
|
||||
if (table == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
JSONArray columns = table.getJSONArray("columns");
|
||||
if (columns == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++)
|
||||
{
|
||||
JSONObject column = columns.getJSONObject(columnIndex);
|
||||
if (column == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
normalizeFlagAlias(column, "isPk", "pk", "primaryKey", "isPrimaryKey", "primary");
|
||||
normalizeFlagAlias(column, "isIncrement", "autoIncrement", "auto_increment", "increment");
|
||||
normalizeFlagAlias(column, "isRequired", "required", "notNull", "not_null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void normalizeFlagAlias(JSONObject column, String canonicalName, String... aliases)
|
||||
{
|
||||
if (column.containsKey(canonicalName))
|
||||
{
|
||||
String normalized = normalizeFlagValue(column.get(canonicalName), null);
|
||||
if (normalized != null)
|
||||
{
|
||||
column.put(canonicalName, normalized);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (String alias : aliases)
|
||||
{
|
||||
if (column.containsKey(alias))
|
||||
{
|
||||
column.put(canonicalName, normalizeFlagValue(column.get(alias), null));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeFlagValue(Object value, String defaultValue)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
if (value instanceof Boolean)
|
||||
{
|
||||
return (Boolean) value ? "1" : "0";
|
||||
}
|
||||
if (value instanceof Number)
|
||||
{
|
||||
return ((Number) value).intValue() == 0 ? "0" : "1";
|
||||
}
|
||||
String text = StringUtils.defaultString(String.valueOf(value)).trim().toLowerCase(Locale.ENGLISH);
|
||||
if ("1".equals(text) || "true".equals(text) || "yes".equals(text) || "y".equals(text))
|
||||
{
|
||||
return "1";
|
||||
}
|
||||
if ("0".equals(text) || "false".equals(text) || "no".equals(text) || "n".equals(text))
|
||||
{
|
||||
return "0";
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private String stripMarkdownFence(String content)
|
||||
{
|
||||
String text = StringUtils.defaultString(content).trim();
|
||||
@@ -142,6 +237,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
{
|
||||
return;
|
||||
}
|
||||
normalizeDatabaseNames(response);
|
||||
int tableSort = 1;
|
||||
for (DatabaseTableDesign table : response.getTables())
|
||||
{
|
||||
@@ -156,11 +252,12 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
{
|
||||
for (DatabaseColumnDesign column : table.getColumns())
|
||||
{
|
||||
column.setColumnType(normalizeColumnType(column.getColumnType()));
|
||||
column.setJavaType(StringUtils.defaultIfEmpty(column.getJavaType(), guessJavaType(column.getColumnType())));
|
||||
column.setJavaField(StringUtils.defaultIfEmpty(column.getJavaField(), toJavaField(column.getColumnName())));
|
||||
column.setIsPk(StringUtils.defaultIfEmpty(column.getIsPk(), "0"));
|
||||
column.setIsIncrement(StringUtils.defaultIfEmpty(column.getIsIncrement(), "0"));
|
||||
column.setIsRequired(StringUtils.defaultIfEmpty(column.getIsRequired(), "0"));
|
||||
column.setIsPk(normalizeFlagValue(column.getIsPk(), "0"));
|
||||
column.setIsIncrement(normalizeFlagValue(column.getIsIncrement(), "0"));
|
||||
column.setIsRequired(normalizeFlagValue(column.getIsRequired(), "0"));
|
||||
column.setIsInsert(StringUtils.defaultIfEmpty(column.getIsInsert(), "1"));
|
||||
column.setIsEdit(StringUtils.defaultIfEmpty(column.getIsEdit(), "1"));
|
||||
column.setIsList(StringUtils.defaultIfEmpty(column.getIsList(), "1"));
|
||||
@@ -171,6 +268,8 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
columnSort++;
|
||||
}
|
||||
}
|
||||
normalizePrimaryKey(table);
|
||||
refreshColumnSort(table);
|
||||
table.setCreateTableSql(ddlBuilder.build(table));
|
||||
if (!validator.isSafeSql(table.getCreateTableSql()))
|
||||
{
|
||||
@@ -182,6 +281,286 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
response.setSql(StringUtils.join(ddlList, "\n\n"));
|
||||
}
|
||||
|
||||
private void normalizeDatabaseNames(DatabaseDesignResponse response)
|
||||
{
|
||||
Set<String> tableNames = new HashSet<String>();
|
||||
int tableIndex = 1;
|
||||
for (DatabaseTableDesign table : response.getTables())
|
||||
{
|
||||
if (table == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
table.setTableName(uniqueDatabaseName(table.getTableName(), "table", tableIndex, tableNames));
|
||||
normalizeColumnNames(table);
|
||||
tableIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
private void normalizeColumnNames(DatabaseTableDesign table)
|
||||
{
|
||||
if (table.getColumns() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Set<String> columnNames = new HashSet<String>();
|
||||
int columnIndex = 1;
|
||||
for (DatabaseColumnDesign column : table.getColumns())
|
||||
{
|
||||
if (column != null)
|
||||
{
|
||||
column.setColumnName(uniqueDatabaseName(column.getColumnName(), "field", columnIndex, columnNames));
|
||||
}
|
||||
columnIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
private String uniqueDatabaseName(String rawName, String fallbackPrefix, int index, Set<String> usedNames)
|
||||
{
|
||||
String baseName = normalizeDatabaseName(rawName, fallbackPrefix + "_" + index);
|
||||
String uniqueName = baseName;
|
||||
int suffix = 2;
|
||||
while (usedNames.contains(uniqueName))
|
||||
{
|
||||
uniqueName = appendNameSuffix(baseName, "_" + suffix);
|
||||
suffix++;
|
||||
}
|
||||
usedNames.add(uniqueName);
|
||||
return uniqueName;
|
||||
}
|
||||
|
||||
private String normalizeDatabaseName(String rawName, String fallbackName)
|
||||
{
|
||||
String name = StringUtils.defaultString(rawName).trim();
|
||||
name = name.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2");
|
||||
name = name.replaceAll("([a-z0-9])([A-Z])", "$1_$2");
|
||||
name = name.toLowerCase(Locale.ENGLISH);
|
||||
name = name.replaceAll("[^a-z0-9]+", "_");
|
||||
name = name.replaceAll("_+", "_").replaceAll("^_+|_+$", "");
|
||||
if (StringUtils.isBlank(name))
|
||||
{
|
||||
name = fallbackName;
|
||||
}
|
||||
if (!name.substring(0, 1).matches("[a-z]"))
|
||||
{
|
||||
name = fallbackName + "_" + name;
|
||||
}
|
||||
if (name.length() < 2)
|
||||
{
|
||||
name = name + "_1";
|
||||
}
|
||||
return trimDatabaseName(name);
|
||||
}
|
||||
|
||||
private String appendNameSuffix(String baseName, String suffix)
|
||||
{
|
||||
int maxBaseLength = Math.max(2, 64 - suffix.length());
|
||||
String trimmedBase = baseName.length() > maxBaseLength ? baseName.substring(0, maxBaseLength) : baseName;
|
||||
trimmedBase = trimmedBase.replaceAll("_+$", "");
|
||||
if (trimmedBase.length() < 2)
|
||||
{
|
||||
trimmedBase = "f_" + trimmedBase;
|
||||
}
|
||||
return trimDatabaseName(trimmedBase + suffix);
|
||||
}
|
||||
|
||||
private String trimDatabaseName(String name)
|
||||
{
|
||||
if (name.length() <= 64)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
String trimmed = name.substring(0, 64).replaceAll("_+$", "");
|
||||
return trimmed.length() < 2 ? "f_1" : trimmed;
|
||||
}
|
||||
|
||||
private void normalizePrimaryKey(DatabaseTableDesign table)
|
||||
{
|
||||
if (table.getColumns() == null || table.getColumns().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
int primaryKeyIndex = -1;
|
||||
for (int i = 0; i < table.getColumns().size(); i++)
|
||||
{
|
||||
DatabaseColumnDesign column = table.getColumns().get(i);
|
||||
if ("1".equals(column.getIsPk()))
|
||||
{
|
||||
if (primaryKeyIndex < 0)
|
||||
{
|
||||
primaryKeyIndex = i;
|
||||
column.setIsRequired("1");
|
||||
}
|
||||
else
|
||||
{
|
||||
column.setIsPk("0");
|
||||
column.setIsIncrement("0");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (primaryKeyIndex >= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
DatabaseColumnDesign idColumn = findColumn(table, "id");
|
||||
if (idColumn == null)
|
||||
{
|
||||
idColumn = createIdPrimaryKeyColumn();
|
||||
table.getColumns().add(0, idColumn);
|
||||
}
|
||||
idColumn.setIsPk("1");
|
||||
idColumn.setIsIncrement("1");
|
||||
idColumn.setIsRequired("1");
|
||||
idColumn.setJavaType("Long");
|
||||
idColumn.setJavaField("id");
|
||||
}
|
||||
|
||||
private DatabaseColumnDesign findColumn(DatabaseTableDesign table, String columnName)
|
||||
{
|
||||
for (DatabaseColumnDesign column : table.getColumns())
|
||||
{
|
||||
if (columnName.equals(column.getColumnName()))
|
||||
{
|
||||
return column;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DatabaseColumnDesign createIdPrimaryKeyColumn()
|
||||
{
|
||||
DatabaseColumnDesign column = new DatabaseColumnDesign();
|
||||
column.setColumnName("id");
|
||||
column.setColumnType("bigint(20)");
|
||||
column.setColumnComment("ID");
|
||||
column.setJavaType("Long");
|
||||
column.setJavaField("id");
|
||||
column.setIsPk("1");
|
||||
column.setIsIncrement("1");
|
||||
column.setIsRequired("1");
|
||||
column.setIsInsert("0");
|
||||
column.setIsEdit("0");
|
||||
column.setIsList("0");
|
||||
column.setIsQuery("0");
|
||||
column.setQueryType("EQ");
|
||||
column.setHtmlType("input");
|
||||
return column;
|
||||
}
|
||||
|
||||
private String normalizeColumnType(String columnType)
|
||||
{
|
||||
String type = StringUtils.defaultString(columnType).trim().toLowerCase(Locale.ENGLISH);
|
||||
type = type.replace("`", "").replaceAll("\\s+", " ");
|
||||
type = type.replaceAll("\\s+unsigned\\b", "");
|
||||
type = type.replaceAll("\\s+(not\\s+null|null|default\\b.*|comment\\b.*|auto_increment\\b.*|primary\\s+key\\b.*)$", "");
|
||||
type = type.trim();
|
||||
if (StringUtils.isBlank(type))
|
||||
{
|
||||
return "varchar(255)";
|
||||
}
|
||||
if (type.startsWith("varchar") || "string".equals(type) || "str".equals(type))
|
||||
{
|
||||
return "varchar(" + boundedTypeNumber(type, 255, 1, 1000) + ")";
|
||||
}
|
||||
if (type.startsWith("char"))
|
||||
{
|
||||
return "char(" + boundedTypeNumber(type, 32, 1, 99) + ")";
|
||||
}
|
||||
if (type.startsWith("bigint") || "long".equals(type) || "long integer".equals(type) || "int64".equals(type))
|
||||
{
|
||||
return "bigint(20)";
|
||||
}
|
||||
if (type.startsWith("int") || "integer".equals(type) || type.startsWith("tinyint")
|
||||
|| type.startsWith("smallint") || type.startsWith("mediumint") || "bool".equals(type)
|
||||
|| "boolean".equals(type))
|
||||
{
|
||||
return "int(11)";
|
||||
}
|
||||
if (type.startsWith("decimal") || "numeric".equals(type) || "number".equals(type)
|
||||
|| "float".equals(type) || "double".equals(type))
|
||||
{
|
||||
return normalizeDecimalType(type);
|
||||
}
|
||||
if ("datetime".equals(type) || "timestamp".equals(type) || "localdatetime".equals(type))
|
||||
{
|
||||
return "datetime";
|
||||
}
|
||||
if ("date".equals(type) || "localdate".equals(type))
|
||||
{
|
||||
return "date";
|
||||
}
|
||||
if ("longtext".equals(type))
|
||||
{
|
||||
return "longtext";
|
||||
}
|
||||
if ("text".equals(type) || "mediumtext".equals(type) || "json".equals(type) || "object".equals(type))
|
||||
{
|
||||
return "text";
|
||||
}
|
||||
return "varchar(255)";
|
||||
}
|
||||
|
||||
private int boundedTypeNumber(String type, int defaultValue, int min, int max)
|
||||
{
|
||||
int start = type.indexOf('(');
|
||||
int end = type.indexOf(')', start + 1);
|
||||
if (start < 0 || end <= start)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
try
|
||||
{
|
||||
int value = Integer.parseInt(type.substring(start + 1, end).split(",")[0].trim());
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeDecimalType(String type)
|
||||
{
|
||||
int precision = 10;
|
||||
int scale = 2;
|
||||
int start = type.indexOf('(');
|
||||
int end = type.indexOf(')', start + 1);
|
||||
if (start >= 0 && end > start)
|
||||
{
|
||||
String[] parts = type.substring(start + 1, end).split(",");
|
||||
try
|
||||
{
|
||||
precision = Integer.parseInt(parts[0].trim());
|
||||
if (parts.length > 1)
|
||||
{
|
||||
scale = Integer.parseInt(parts[1].trim());
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
precision = 10;
|
||||
scale = 2;
|
||||
}
|
||||
}
|
||||
precision = Math.max(1, Math.min(65, precision));
|
||||
scale = Math.max(0, Math.min(30, scale));
|
||||
scale = Math.min(scale, precision);
|
||||
return "decimal(" + precision + "," + scale + ")";
|
||||
}
|
||||
|
||||
private void refreshColumnSort(DatabaseTableDesign table)
|
||||
{
|
||||
if (table.getColumns() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int sort = 1;
|
||||
for (DatabaseColumnDesign column : table.getColumns())
|
||||
{
|
||||
column.setSort(sort++);
|
||||
}
|
||||
}
|
||||
|
||||
private void persistDatabase(Long userId, Long projectId, DatabaseDesignResponse response)
|
||||
{
|
||||
frontProjectColumnMapper.deleteColumnsByProjectId(projectId);
|
||||
@@ -198,6 +577,15 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
}
|
||||
}
|
||||
|
||||
private void markDatabaseGenerated(Long projectId)
|
||||
{
|
||||
FrontProject project = new FrontProject();
|
||||
project.setProjectId(projectId);
|
||||
project.setGenerateStatus("1");
|
||||
project.setPreviewStatus("0");
|
||||
frontProjectMapper.updateFrontProject(project);
|
||||
}
|
||||
|
||||
private FrontProjectGeneration newGeneration(Long projectId, Long userId, String prompt)
|
||||
{
|
||||
FrontProjectGeneration generation = new FrontProjectGeneration();
|
||||
|
||||
@@ -12,9 +12,15 @@ import com.ruoyi.common.exception.ServiceException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@Service
|
||||
public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewService
|
||||
@@ -55,6 +61,24 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
|
||||
return genProjectService.downloadStructure(genProject, templateType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] downloadAll(Long userId, Long projectId)
|
||||
{
|
||||
GenProject genProject = loadGenProject(userId, projectId);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zip = new ZipOutputStream(outputStream))
|
||||
{
|
||||
appendZip(zip, "backend", genProjectService.downloadStructure(genProject, "backend"));
|
||||
appendZip(zip, "front-web", genProjectService.downloadStructure(genProject, "frontend"));
|
||||
appendZip(zip, "admin-web", genProjectService.downloadStructure(genProject, "admin_frontend"));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new ServiceException("生成项目源码压缩包失败");
|
||||
}
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int markPreviewReady(Long userId, Long projectId)
|
||||
{
|
||||
@@ -83,4 +107,59 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
|
||||
}
|
||||
return FrontProjectConverter.toGenProject(frontProject);
|
||||
}
|
||||
|
||||
private void appendZip(ZipOutputStream targetZip, String rootFolder, byte[] sourceData) throws IOException
|
||||
{
|
||||
ZipInputStream sourceZip = new ZipInputStream(new ByteArrayInputStream(sourceData));
|
||||
ZipEntry sourceEntry;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((sourceEntry = sourceZip.getNextEntry()) != null)
|
||||
{
|
||||
String entryName = normalizeEntryName(rootFolder, sourceEntry.getName());
|
||||
if (entryName == null)
|
||||
{
|
||||
sourceZip.closeEntry();
|
||||
continue;
|
||||
}
|
||||
if (sourceEntry.isDirectory())
|
||||
{
|
||||
if (!entryName.endsWith("/"))
|
||||
{
|
||||
entryName = entryName + "/";
|
||||
}
|
||||
targetZip.putNextEntry(new ZipEntry(entryName));
|
||||
targetZip.closeEntry();
|
||||
}
|
||||
else
|
||||
{
|
||||
targetZip.putNextEntry(new ZipEntry(entryName));
|
||||
int length;
|
||||
while ((length = sourceZip.read(buffer)) > -1)
|
||||
{
|
||||
targetZip.write(buffer, 0, length);
|
||||
}
|
||||
targetZip.closeEntry();
|
||||
}
|
||||
sourceZip.closeEntry();
|
||||
}
|
||||
sourceZip.close();
|
||||
}
|
||||
|
||||
private String normalizeEntryName(String rootFolder, String entryName)
|
||||
{
|
||||
if (entryName == null || entryName.trim().length() == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String normalized = entryName.replace('\\', '/');
|
||||
while (normalized.startsWith("/"))
|
||||
{
|
||||
normalized = normalized.substring(1);
|
||||
}
|
||||
if (normalized.startsWith("../") || normalized.contains("/../"))
|
||||
{
|
||||
throw new ServiceException("项目源码压缩包路径不合法");
|
||||
}
|
||||
return rootFolder + "/" + normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -34,6 +35,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
private static final Pattern VARCHAR_TYPE_PATTERN = Pattern.compile("^varchar\\((\\d{1,5})\\)$");
|
||||
private static final Pattern CHAR_TYPE_PATTERN = Pattern.compile("^char\\((\\d{1,3})\\)$");
|
||||
private static final Pattern DECIMAL_TYPE_PATTERN = Pattern.compile("^decimal\\((\\d{1,2}),(\\d{1,2})\\)$");
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
@Autowired
|
||||
private FrontProjectMapper frontProjectMapper;
|
||||
@@ -129,7 +131,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
public DatabaseDesignResponse getDatabase(Long userId, Long projectId)
|
||||
{
|
||||
assertFrontUserLoggedIn(userId);
|
||||
getProject(userId, projectId);
|
||||
FrontProject project = getProject(userId, projectId);
|
||||
DatabaseDesignResponse response = new DatabaseDesignResponse();
|
||||
response.setProjectId(projectId);
|
||||
List<DatabaseTableDesign> tables = new ArrayList<DatabaseTableDesign>();
|
||||
@@ -146,6 +148,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
}
|
||||
response.setTables(tables);
|
||||
response.setSql(joinCreateTableSql(tables));
|
||||
response.setErDiagram(readJson(project.getErDiagram()));
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -168,6 +171,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
frontProjectColumnMapper.insertFrontProjectColumn(column);
|
||||
}
|
||||
}
|
||||
markDatabaseGenerated(projectId, writeJson(request.getErDiagram()));
|
||||
return getDatabase(userId, projectId);
|
||||
}
|
||||
|
||||
@@ -289,6 +293,48 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
return StringUtils.join(sqlList, "\n\n");
|
||||
}
|
||||
|
||||
private void markDatabaseGenerated(Long projectId, String erDiagram)
|
||||
{
|
||||
FrontProject project = new FrontProject();
|
||||
project.setProjectId(projectId);
|
||||
project.setGenerateStatus("1");
|
||||
project.setPreviewStatus("0");
|
||||
project.setErDiagram(erDiagram);
|
||||
frontProjectMapper.updateFrontProject(project);
|
||||
}
|
||||
|
||||
private Object readJson(String json)
|
||||
{
|
||||
if (StringUtils.isEmpty(json))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
return OBJECT_MAPPER.readValue(json, Object.class);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String writeJson(Object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
return value instanceof String ? (String) value : OBJECT_MAPPER.writeValueAsString(value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ServiceException("ER图草稿保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source)
|
||||
{
|
||||
FrontProjectTable table = new FrontProjectTable();
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -18,6 +29,34 @@ import com.ruoyi.generator.config.DeepSeekProperties;
|
||||
@Component
|
||||
public class HttpDeepSeekClient implements IDeepSeekClient
|
||||
{
|
||||
private static final HostnameVerifier TRUST_ALL_HOSTNAME_VERIFIER = new HostnameVerifier()
|
||||
{
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private static final X509TrustManager TRUST_ALL_TRUST_MANAGER = new X509TrustManager()
|
||||
{
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers()
|
||||
{
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
};
|
||||
|
||||
private static final String SYSTEM_PROMPT = "你是企业管理系统数据库设计助手,只返回JSON。";
|
||||
|
||||
@Autowired
|
||||
@@ -57,10 +96,58 @@ public class HttpDeepSeekClient implements IDeepSeekClient
|
||||
private RestTemplate restTemplate()
|
||||
{
|
||||
int timeout = deepSeekProperties.getTimeout() == null ? 60000 : deepSeekProperties.getTimeout();
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout((int) TimeUnit.MILLISECONDS.toMillis(timeout));
|
||||
factory.setReadTimeout((int) TimeUnit.MILLISECONDS.toMillis(timeout));
|
||||
return new RestTemplate(factory);
|
||||
return new RestTemplate(requestFactory(timeout));
|
||||
}
|
||||
|
||||
SimpleClientHttpRequestFactory requestFactory(int timeout)
|
||||
{
|
||||
SimpleClientHttpRequestFactory factory = deepSeekProperties.isSkipSslVerify()
|
||||
? trustAllRequestFactory()
|
||||
: new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(timeout);
|
||||
factory.setReadTimeout(timeout);
|
||||
return factory;
|
||||
}
|
||||
|
||||
private SimpleClientHttpRequestFactory trustAllRequestFactory()
|
||||
{
|
||||
return new TrustAllClientHttpRequestFactory(trustAllSslContext());
|
||||
}
|
||||
|
||||
private SSLContext trustAllSslContext()
|
||||
{
|
||||
try
|
||||
{
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, new TrustManager[] { TRUST_ALL_TRUST_MANAGER }, new SecureRandom());
|
||||
return sslContext;
|
||||
}
|
||||
catch (NoSuchAlgorithmException | KeyManagementException e)
|
||||
{
|
||||
throw new ServiceException("DeepSeek SSL 证书忽略配置初始化失败").setDetailMessage(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static class TrustAllClientHttpRequestFactory extends SimpleClientHttpRequestFactory
|
||||
{
|
||||
private final SSLContext sslContext;
|
||||
|
||||
private TrustAllClientHttpRequestFactory(SSLContext sslContext)
|
||||
{
|
||||
this.sslContext = sslContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException
|
||||
{
|
||||
super.prepareConnection(connection, httpMethod);
|
||||
if (connection instanceof HttpsURLConnection)
|
||||
{
|
||||
HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
|
||||
httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory());
|
||||
httpsConnection.setHostnameVerifier(TRUST_ALL_HOSTNAME_VERIFIER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String chatUrl()
|
||||
|
||||
@@ -13,5 +13,7 @@ public interface IFrontProjectPreviewService
|
||||
|
||||
public byte[] download(Long userId, Long projectId, String templateType);
|
||||
|
||||
public byte[] downloadAll(Long userId, Long projectId);
|
||||
|
||||
public int markPreviewReady(Long userId, Long projectId);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="backFramework" column="back_framework" />
|
||||
<result property="generateStatus" column="generate_status" />
|
||||
<result property="previewStatus" column="preview_status" />
|
||||
<result property="erDiagram" column="er_diagram" />
|
||||
<result property="status" column="status" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
@@ -24,7 +25,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFrontProjectVo">
|
||||
select project_id, user_id, project_name, project_file_name, package_name, version, project_desc, industry_template, front_framework, back_framework, generate_status, preview_status, status, create_time, update_time, remark from front_project
|
||||
select project_id, user_id, project_name, project_file_name, package_name, version, project_desc, industry_template, front_framework, back_framework, generate_status, preview_status, er_diagram, status, create_time, update_time, remark from front_project
|
||||
</sql>
|
||||
|
||||
<select id="selectFrontProjectById" parameterType="Long" resultMap="FrontProjectResult">
|
||||
@@ -64,6 +65,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="backFramework != null">back_framework,</if>
|
||||
<if test="generateStatus != null">generate_status,</if>
|
||||
<if test="previewStatus != null">preview_status,</if>
|
||||
<if test="erDiagram != null">er_diagram,</if>
|
||||
<if test="status != null">status,</if>
|
||||
create_time,
|
||||
<if test="remark != null">remark,</if>
|
||||
@@ -80,6 +82,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="backFramework != null">#{backFramework},</if>
|
||||
<if test="generateStatus != null">#{generateStatus},</if>
|
||||
<if test="previewStatus != null">#{previewStatus},</if>
|
||||
<if test="erDiagram != null">#{erDiagram},</if>
|
||||
<if test="status != null">#{status},</if>
|
||||
sysdate(),
|
||||
<if test="remark != null">#{remark},</if>
|
||||
@@ -99,6 +102,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="backFramework != null">back_framework = #{backFramework},</if>
|
||||
<if test="generateStatus != null">generate_status = #{generateStatus},</if>
|
||||
<if test="previewStatus != null">preview_status = #{previewStatus},</if>
|
||||
<if test="erDiagram != null">er_diagram = #{erDiagram},</if>
|
||||
<if test="status != null">status = #{status},</if>
|
||||
update_time = sysdate(),
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.generator.mapper.SourceProjectMapper">
|
||||
|
||||
<resultMap type="SourceProject" id="SourceProjectResult">
|
||||
<id property="projectId" column="project_id" />
|
||||
<result property="slug" column="slug" />
|
||||
<result property="projectName" column="project_name" />
|
||||
<result property="category" column="category" />
|
||||
<result property="coverText" column="cover_text" />
|
||||
<result property="summary" column="summary" />
|
||||
<result property="descriptionMd" column="description_md" />
|
||||
<result property="price" column="price" />
|
||||
<result property="viewCount" column="view_count" />
|
||||
<result property="tags" column="tags" />
|
||||
<result property="modules" column="modules" />
|
||||
<result property="scenes" column="scenes" />
|
||||
<result property="resourceName" column="resource_name" />
|
||||
<result property="resourceUrl" column="resource_url" />
|
||||
<result property="resourceVersion" column="resource_version" />
|
||||
<result property="resourceSize" column="resource_size" />
|
||||
<result property="sort" column="sort" />
|
||||
<result property="status" column="status" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectSourceProjectVo">
|
||||
select project_id, slug, project_name, category, cover_text, summary, description_md, price, view_count,
|
||||
tags, modules, scenes, resource_name, resource_url, resource_version, resource_size, sort, status,
|
||||
del_flag, create_by, create_time, update_by, update_time, remark
|
||||
from source_project
|
||||
</sql>
|
||||
|
||||
<select id="selectSourceProjectByProjectId" parameterType="Long" resultMap="SourceProjectResult">
|
||||
<include refid="selectSourceProjectVo"/>
|
||||
where project_id = #{projectId} and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectSourceProjectBySlug" parameterType="String" resultMap="SourceProjectResult">
|
||||
<include refid="selectSourceProjectVo"/>
|
||||
where slug = #{slug} and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectSourceProjectList" parameterType="SourceProject" resultMap="SourceProjectResult">
|
||||
<include refid="selectSourceProjectVo"/>
|
||||
<where>
|
||||
and del_flag = '0'
|
||||
<if test="projectName != null and projectName != ''"> and project_name like concat('%', #{projectName}, '%')</if>
|
||||
<if test="slug != null and slug != ''"> and slug like concat('%', #{slug}, '%')</if>
|
||||
<if test="category != null and category != '' and category != 'all'"> and category = #{category}</if>
|
||||
<if test="status != null and status != ''"> and status = #{status}</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
and (
|
||||
project_name like concat('%', #{keyword}, '%')
|
||||
or summary like concat('%', #{keyword}, '%')
|
||||
or tags like concat('%', #{keyword}, '%')
|
||||
or modules like concat('%', #{keyword}, '%')
|
||||
or scenes like concat('%', #{keyword}, '%')
|
||||
)
|
||||
</if>
|
||||
</where>
|
||||
order by sort asc, update_time desc, create_time desc
|
||||
</select>
|
||||
|
||||
<select id="selectPublicSourceProjectList" parameterType="SourceProject" resultMap="SourceProjectResult">
|
||||
<include refid="selectSourceProjectVo"/>
|
||||
<where>
|
||||
and del_flag = '0'
|
||||
and status = '0'
|
||||
<if test="category != null and category != '' and category != 'all'"> and category = #{category}</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
and (
|
||||
project_name like concat('%', #{keyword}, '%')
|
||||
or summary like concat('%', #{keyword}, '%')
|
||||
or tags like concat('%', #{keyword}, '%')
|
||||
or modules like concat('%', #{keyword}, '%')
|
||||
or scenes like concat('%', #{keyword}, '%')
|
||||
)
|
||||
</if>
|
||||
</where>
|
||||
order by sort asc, update_time desc, create_time desc
|
||||
</select>
|
||||
|
||||
<insert id="insertSourceProject" parameterType="SourceProject" useGeneratedKeys="true" keyProperty="projectId">
|
||||
insert into source_project
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
slug,
|
||||
project_name,
|
||||
<if test="category != null">category,</if>
|
||||
<if test="coverText != null">cover_text,</if>
|
||||
<if test="summary != null">summary,</if>
|
||||
<if test="descriptionMd != null">description_md,</if>
|
||||
<if test="price != null">price,</if>
|
||||
<if test="viewCount != null">view_count,</if>
|
||||
<if test="tags != null">tags,</if>
|
||||
<if test="modules != null">modules,</if>
|
||||
<if test="scenes != null">scenes,</if>
|
||||
<if test="resourceName != null">resource_name,</if>
|
||||
<if test="resourceUrl != null">resource_url,</if>
|
||||
<if test="resourceVersion != null">resource_version,</if>
|
||||
<if test="resourceSize != null">resource_size,</if>
|
||||
<if test="sort != null">sort,</if>
|
||||
<if test="status != null">status,</if>
|
||||
<if test="delFlag != null">del_flag,</if>
|
||||
<if test="createBy != null">create_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
<if test="updateBy != null">update_by,</if>
|
||||
<if test="updateTime != null">update_time,</if>
|
||||
<if test="remark != null">remark,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
#{slug},
|
||||
#{projectName},
|
||||
<if test="category != null">#{category},</if>
|
||||
<if test="coverText != null">#{coverText},</if>
|
||||
<if test="summary != null">#{summary},</if>
|
||||
<if test="descriptionMd != null">#{descriptionMd},</if>
|
||||
<if test="price != null">#{price},</if>
|
||||
<if test="viewCount != null">#{viewCount},</if>
|
||||
<if test="tags != null">#{tags},</if>
|
||||
<if test="modules != null">#{modules},</if>
|
||||
<if test="scenes != null">#{scenes},</if>
|
||||
<if test="resourceName != null">#{resourceName},</if>
|
||||
<if test="resourceUrl != null">#{resourceUrl},</if>
|
||||
<if test="resourceVersion != null">#{resourceVersion},</if>
|
||||
<if test="resourceSize != null">#{resourceSize},</if>
|
||||
<if test="sort != null">#{sort},</if>
|
||||
<if test="status != null">#{status},</if>
|
||||
<if test="delFlag != null">#{delFlag},</if>
|
||||
<if test="createBy != null">#{createBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
<if test="updateBy != null">#{updateBy},</if>
|
||||
<if test="updateTime != null">#{updateTime},</if>
|
||||
<if test="remark != null">#{remark},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<update id="updateSourceProject" parameterType="SourceProject">
|
||||
update source_project
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="slug != null">slug = #{slug},</if>
|
||||
<if test="projectName != null">project_name = #{projectName},</if>
|
||||
<if test="category != null">category = #{category},</if>
|
||||
<if test="coverText != null">cover_text = #{coverText},</if>
|
||||
<if test="summary != null">summary = #{summary},</if>
|
||||
<if test="descriptionMd != null">description_md = #{descriptionMd},</if>
|
||||
<if test="price != null">price = #{price},</if>
|
||||
<if test="viewCount != null">view_count = #{viewCount},</if>
|
||||
<if test="tags != null">tags = #{tags},</if>
|
||||
<if test="modules != null">modules = #{modules},</if>
|
||||
<if test="scenes != null">scenes = #{scenes},</if>
|
||||
<if test="resourceName != null">resource_name = #{resourceName},</if>
|
||||
<if test="resourceUrl != null">resource_url = #{resourceUrl},</if>
|
||||
<if test="resourceVersion != null">resource_version = #{resourceVersion},</if>
|
||||
<if test="resourceSize != null">resource_size = #{resourceSize},</if>
|
||||
<if test="sort != null">sort = #{sort},</if>
|
||||
<if test="status != null">status = #{status},</if>
|
||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||
<if test="updateTime != null">update_time = #{updateTime},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</trim>
|
||||
where project_id = #{projectId}
|
||||
</update>
|
||||
|
||||
<update id="deleteSourceProjectByProjectIds" parameterType="Long">
|
||||
update source_project set del_flag = '2', update_time = sysdate()
|
||||
where project_id in
|
||||
<foreach item="projectId" collection="array" open="(" separator="," close=")">
|
||||
#{projectId}
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
<update id="changeSourceProjectStatus" parameterType="SourceProject">
|
||||
update source_project
|
||||
set status = #{status}, update_time = #{updateTime}
|
||||
where project_id = #{projectId}
|
||||
</update>
|
||||
|
||||
<update id="increaseViewCount" parameterType="Long">
|
||||
update source_project set view_count = ifnull(view_count, 0) + 1 where project_id = #{projectId}
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.generator.mapper.SourcePurchaseMapper">
|
||||
|
||||
<resultMap type="SourcePurchase" id="SourcePurchaseResult">
|
||||
<id property="purchaseId" column="purchase_id" />
|
||||
<result property="projectId" column="project_id" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="orderNo" column="order_no" />
|
||||
<result property="amount" column="amount" />
|
||||
<result property="payStatus" column="pay_status" />
|
||||
<result property="resourceStatus" column="resource_status" />
|
||||
<result property="purchaseTime" column="purchase_time" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
<result property="projectName" column="project_name" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectSourcePurchaseVo">
|
||||
select p.purchase_id, p.project_id, p.user_id, p.order_no, p.amount, p.pay_status, p.resource_status,
|
||||
p.purchase_time, p.create_by, p.create_time, p.update_by, p.update_time, p.remark,
|
||||
sp.project_name
|
||||
from source_purchase p
|
||||
left join source_project sp on sp.project_id = p.project_id
|
||||
</sql>
|
||||
|
||||
<select id="selectSourcePurchaseByPurchaseId" parameterType="Long" resultMap="SourcePurchaseResult">
|
||||
<include refid="selectSourcePurchaseVo"/>
|
||||
where p.purchase_id = #{purchaseId}
|
||||
</select>
|
||||
|
||||
<select id="selectPaidPurchase" resultMap="SourcePurchaseResult">
|
||||
<include refid="selectSourcePurchaseVo"/>
|
||||
where p.user_id = #{userId} and p.project_id = #{projectId} and p.pay_status = '1'
|
||||
order by p.purchase_time desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectSourcePurchaseList" parameterType="SourcePurchase" resultMap="SourcePurchaseResult">
|
||||
<include refid="selectSourcePurchaseVo"/>
|
||||
<where>
|
||||
<if test="projectId != null"> and p.project_id = #{projectId}</if>
|
||||
<if test="userId != null"> and p.user_id = #{userId}</if>
|
||||
<if test="orderNo != null and orderNo != ''"> and p.order_no like concat('%', #{orderNo}, '%')</if>
|
||||
<if test="payStatus != null and payStatus != ''"> and p.pay_status = #{payStatus}</if>
|
||||
<if test="resourceStatus != null and resourceStatus != ''"> and p.resource_status = #{resourceStatus}</if>
|
||||
<if test="projectName != null and projectName != ''"> and sp.project_name like concat('%', #{projectName}, '%')</if>
|
||||
</where>
|
||||
order by p.purchase_time desc, p.create_time desc
|
||||
</select>
|
||||
|
||||
<select id="selectSourcePurchasesByUserId" parameterType="Long" resultMap="SourcePurchaseResult">
|
||||
<include refid="selectSourcePurchaseVo"/>
|
||||
where p.user_id = #{userId}
|
||||
order by p.purchase_time desc, p.create_time desc
|
||||
</select>
|
||||
|
||||
<insert id="insertSourcePurchase" parameterType="SourcePurchase" useGeneratedKeys="true" keyProperty="purchaseId">
|
||||
insert into source_purchase
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
project_id,
|
||||
user_id,
|
||||
order_no,
|
||||
amount,
|
||||
pay_status,
|
||||
resource_status,
|
||||
purchase_time,
|
||||
<if test="createBy != null">create_by,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
<if test="updateBy != null">update_by,</if>
|
||||
<if test="updateTime != null">update_time,</if>
|
||||
<if test="remark != null">remark,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
#{projectId},
|
||||
#{userId},
|
||||
#{orderNo},
|
||||
#{amount},
|
||||
#{payStatus},
|
||||
#{resourceStatus},
|
||||
#{purchaseTime},
|
||||
<if test="createBy != null">#{createBy},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
<if test="updateBy != null">#{updateBy},</if>
|
||||
<if test="updateTime != null">#{updateTime},</if>
|
||||
<if test="remark != null">#{remark},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<update id="updateSourcePurchase" parameterType="SourcePurchase">
|
||||
update source_purchase
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="payStatus != null">pay_status = #{payStatus},</if>
|
||||
<if test="resourceStatus != null">resource_status = #{resourceStatus},</if>
|
||||
<if test="purchaseTime != null">purchase_time = #{purchaseTime},</if>
|
||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||
<if test="updateTime != null">update_time = #{updateTime},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
</trim>
|
||||
where purchase_id = #{purchaseId}
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.ruoyi.generator.service;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.generator.domain.SourceProject;
|
||||
import com.ruoyi.generator.domain.SourcePurchase;
|
||||
import com.ruoyi.generator.domain.source.SourceProjectResponse;
|
||||
import com.ruoyi.generator.mapper.SourceProjectMapper;
|
||||
import com.ruoyi.generator.mapper.SourcePurchaseMapper;
|
||||
|
||||
public class SourceProjectServiceImplTest
|
||||
{
|
||||
private SourceProjectServiceImpl service;
|
||||
|
||||
@Mock
|
||||
private SourceProjectMapper sourceProjectMapper;
|
||||
@Mock
|
||||
private SourcePurchaseMapper sourcePurchaseMapper;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
MockitoAnnotations.initMocks(this);
|
||||
service = new SourceProjectServiceImpl();
|
||||
setField("sourceProjectMapper", sourceProjectMapper);
|
||||
setField("sourcePurchaseMapper", sourcePurchaseMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listPublicProjectsFormatsCatalogFieldsAndPurchaseState()
|
||||
{
|
||||
SourceProject query = new SourceProject();
|
||||
SourceProject project = project("0");
|
||||
project.setTags("[\"Spring Boot\",\"Vue2\"]");
|
||||
project.setModules("[\"Dealer\",\"Repair\"]");
|
||||
project.setScenes("[\"Aftermarket\"]");
|
||||
when(sourceProjectMapper.selectPublicSourceProjectList(query)).thenReturn(Collections.singletonList(project));
|
||||
when(sourcePurchaseMapper.selectPaidPurchase(7L, 10L)).thenReturn(purchase());
|
||||
|
||||
List<SourceProjectResponse> projects = service.listPublicProjects(7L, query);
|
||||
|
||||
assertEquals(1, projects.size());
|
||||
SourceProjectResponse response = projects.get(0);
|
||||
assertEquals(Long.valueOf(10L), response.getProjectId());
|
||||
assertEquals("dms-platform", response.getSlug());
|
||||
assertEquals("Commercial DMS", response.getProjectName());
|
||||
assertEquals("¥399", response.getPriceLabel());
|
||||
assertEquals(Long.valueOf(2860L), response.getViewCount());
|
||||
assertEquals("Spring Boot", response.getTags().get(0));
|
||||
assertEquals("Repair", response.getModules().get(1));
|
||||
assertTrue(response.isPurchased());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void purchaseCreatesPaidPurchaseForActiveProject()
|
||||
{
|
||||
SourceProject project = project("0");
|
||||
project.setResourceUrl("https://example.com/dms.zip");
|
||||
when(sourceProjectMapper.selectSourceProjectByProjectId(10L)).thenReturn(project);
|
||||
|
||||
SourcePurchase purchase = service.purchase(7L, 10L);
|
||||
|
||||
ArgumentCaptor<SourcePurchase> captor = ArgumentCaptor.forClass(SourcePurchase.class);
|
||||
verify(sourcePurchaseMapper).insertSourcePurchase(captor.capture());
|
||||
assertEquals(Long.valueOf(7L), captor.getValue().getUserId());
|
||||
assertEquals(Long.valueOf(10L), captor.getValue().getProjectId());
|
||||
assertEquals(Long.valueOf(39900L), captor.getValue().getAmount());
|
||||
assertEquals("1", captor.getValue().getPayStatus());
|
||||
assertEquals("1", captor.getValue().getResourceStatus());
|
||||
assertTrue(captor.getValue().getOrderNo().startsWith("SRC"));
|
||||
assertEquals(captor.getValue(), purchase);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void purchaseRejectsOfflineProject()
|
||||
{
|
||||
when(sourceProjectMapper.selectSourceProjectByProjectId(10L)).thenReturn(project("1"));
|
||||
|
||||
ServiceException exception = expectServiceException(new ThrowingRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
service.purchase(7L, 10L);
|
||||
}
|
||||
});
|
||||
|
||||
assertEquals("源码项目未上架或不存在", exception.getMessage());
|
||||
}
|
||||
|
||||
private SourceProject project(String status)
|
||||
{
|
||||
SourceProject project = new SourceProject();
|
||||
project.setProjectId(10L);
|
||||
project.setSlug("dms-platform");
|
||||
project.setProjectName("Commercial DMS");
|
||||
project.setCategory("enterprise");
|
||||
project.setCoverText("DMS");
|
||||
project.setSummary("DMS source");
|
||||
project.setDescriptionMd("# DMS");
|
||||
project.setPrice(39900L);
|
||||
project.setViewCount(2860L);
|
||||
project.setStatus(status);
|
||||
return project;
|
||||
}
|
||||
|
||||
private SourcePurchase purchase()
|
||||
{
|
||||
SourcePurchase purchase = new SourcePurchase();
|
||||
purchase.setPurchaseId(20L);
|
||||
return purchase;
|
||||
}
|
||||
|
||||
private void setField(String name, Object value) throws Exception
|
||||
{
|
||||
Field field = SourceProjectServiceImpl.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(service, value);
|
||||
}
|
||||
|
||||
private ServiceException expectServiceException(ThrowingRunnable runnable)
|
||||
{
|
||||
try
|
||||
{
|
||||
runnable.run();
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
return e;
|
||||
}
|
||||
throw new AssertionError("Expected ServiceException");
|
||||
}
|
||||
|
||||
private interface ThrowingRunnable
|
||||
{
|
||||
void run();
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,94 @@ public class AiGenerateServiceImplTest
|
||||
assertEquals(2, columnCaptor.getAllValues().size());
|
||||
assertEquals("1", generationCaptor.getValue().getSuccess());
|
||||
assertTrue(response.getSql().contains("CREATE TABLE `car_info`"));
|
||||
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
||||
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
|
||||
assertEquals("1", projectCaptor.getValue().getGenerateStatus());
|
||||
assertEquals("0", projectCaptor.getValue().getPreviewStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseAddsIdPrimaryKeyWhenAiOmitsPrimaryKey()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(responseWithoutPrimaryKey());
|
||||
|
||||
DatabaseDesignResponse response = service.generateDatabase(7L, 10L, request());
|
||||
|
||||
ArgumentCaptor<FrontProjectColumn> columnCaptor = ArgumentCaptor.forClass(FrontProjectColumn.class);
|
||||
verify(frontProjectColumnMapper, org.mockito.Mockito.times(2)).insertFrontProjectColumn(columnCaptor.capture());
|
||||
FrontProjectColumn idColumn = columnCaptor.getAllValues().get(0);
|
||||
assertEquals("id", idColumn.getColumnName());
|
||||
assertEquals("1", idColumn.getIsPk());
|
||||
assertEquals("1", idColumn.getIsIncrement());
|
||||
assertTrue(response.getSql().contains("PRIMARY KEY (`id`)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseAcceptsPrimaryKeyAliasFromAi()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(responseWithPrimaryKeyAlias());
|
||||
|
||||
service.generateDatabase(7L, 10L, request());
|
||||
|
||||
ArgumentCaptor<FrontProjectColumn> columnCaptor = ArgumentCaptor.forClass(FrontProjectColumn.class);
|
||||
verify(frontProjectColumnMapper, org.mockito.Mockito.times(2)).insertFrontProjectColumn(columnCaptor.capture());
|
||||
FrontProjectColumn orderId = columnCaptor.getAllValues().get(0);
|
||||
assertEquals("order_id", orderId.getColumnName());
|
||||
assertEquals("1", orderId.getIsPk());
|
||||
assertEquals("1", orderId.getIsIncrement());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseKeepsOnlyFirstPrimaryKeyWhenAiReturnsMultiple()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(responseWithMultiplePrimaryKeys());
|
||||
|
||||
service.generateDatabase(7L, 10L, request());
|
||||
|
||||
ArgumentCaptor<FrontProjectColumn> columnCaptor = ArgumentCaptor.forClass(FrontProjectColumn.class);
|
||||
verify(frontProjectColumnMapper, org.mockito.Mockito.times(2)).insertFrontProjectColumn(columnCaptor.capture());
|
||||
assertEquals("1", columnCaptor.getAllValues().get(0).getIsPk());
|
||||
assertEquals("0", columnCaptor.getAllValues().get(1).getIsPk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseNormalizesUnsafeAiColumnNamesBeforeValidation()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(responseWithUnsafeColumnNames());
|
||||
|
||||
service.generateDatabase(7L, 10L, request());
|
||||
|
||||
ArgumentCaptor<FrontProjectColumn> columnCaptor = ArgumentCaptor.forClass(FrontProjectColumn.class);
|
||||
verify(frontProjectColumnMapper, org.mockito.Mockito.times(4)).insertFrontProjectColumn(columnCaptor.capture());
|
||||
assertEquals("order_id", columnCaptor.getAllValues().get(0).getColumnName());
|
||||
assertEquals("customer_name", columnCaptor.getAllValues().get(1).getColumnName());
|
||||
assertEquals("field_3", columnCaptor.getAllValues().get(2).getColumnName());
|
||||
assertEquals("customer_name_2", columnCaptor.getAllValues().get(3).getColumnName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseNormalizesUnsupportedAiColumnTypesBeforeValidation()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(responseWithUnsupportedColumnTypes());
|
||||
|
||||
service.generateDatabase(7L, 10L, request());
|
||||
|
||||
ArgumentCaptor<FrontProjectColumn> columnCaptor = ArgumentCaptor.forClass(FrontProjectColumn.class);
|
||||
verify(frontProjectColumnMapper, org.mockito.Mockito.times(8)).insertFrontProjectColumn(columnCaptor.capture());
|
||||
assertEquals("bigint(20)", columnCaptor.getAllValues().get(0).getColumnType());
|
||||
assertEquals("int(11)", columnCaptor.getAllValues().get(1).getColumnType());
|
||||
assertEquals("varchar(255)", columnCaptor.getAllValues().get(2).getColumnType());
|
||||
assertEquals("int(11)", columnCaptor.getAllValues().get(3).getColumnType());
|
||||
assertEquals("datetime", columnCaptor.getAllValues().get(4).getColumnType());
|
||||
assertEquals("text", columnCaptor.getAllValues().get(5).getColumnType());
|
||||
assertEquals("decimal(10,2)", columnCaptor.getAllValues().get(6).getColumnType());
|
||||
assertEquals("varchar(1000)", columnCaptor.getAllValues().get(7).getColumnType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -200,4 +288,134 @@ public class AiGenerateServiceImplTest
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String responseWithoutPrimaryKey()
|
||||
{
|
||||
return "{\n"
|
||||
+ " \"tables\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"tableName\": \"customer_info\",\n"
|
||||
+ " \"tableComment\": \"Customer info\",\n"
|
||||
+ " \"columns\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"columnName\": \"customer_name\",\n"
|
||||
+ " \"columnType\": \"varchar(64)\",\n"
|
||||
+ " \"isRequired\": \"1\",\n"
|
||||
+ " \"columnComment\": \"Customer name\"\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String responseWithPrimaryKeyAlias()
|
||||
{
|
||||
return "{\n"
|
||||
+ " \"tables\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"tableName\": \"order_info\",\n"
|
||||
+ " \"tableComment\": \"Order info\",\n"
|
||||
+ " \"columns\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"columnName\": \"order_id\",\n"
|
||||
+ " \"columnType\": \"bigint(20)\",\n"
|
||||
+ " \"primaryKey\": true,\n"
|
||||
+ " \"autoIncrement\": true,\n"
|
||||
+ " \"columnComment\": \"Order ID\"\n"
|
||||
+ " },\n"
|
||||
+ " {\n"
|
||||
+ " \"columnName\": \"order_no\",\n"
|
||||
+ " \"columnType\": \"varchar(64)\",\n"
|
||||
+ " \"columnComment\": \"Order no\"\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String responseWithMultiplePrimaryKeys()
|
||||
{
|
||||
return "{\n"
|
||||
+ " \"tables\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"tableName\": \"product_info\",\n"
|
||||
+ " \"tableComment\": \"Product info\",\n"
|
||||
+ " \"columns\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"columnName\": \"product_id\",\n"
|
||||
+ " \"columnType\": \"bigint(20)\",\n"
|
||||
+ " \"isPk\": \"1\",\n"
|
||||
+ " \"columnComment\": \"Product ID\"\n"
|
||||
+ " },\n"
|
||||
+ " {\n"
|
||||
+ " \"columnName\": \"sku_id\",\n"
|
||||
+ " \"columnType\": \"bigint(20)\",\n"
|
||||
+ " \"isPk\": \"1\",\n"
|
||||
+ " \"columnComment\": \"SKU ID\"\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String responseWithUnsafeColumnNames()
|
||||
{
|
||||
return "{\n"
|
||||
+ " \"tables\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"tableName\": \"order_info\",\n"
|
||||
+ " \"tableComment\": \"Order info\",\n"
|
||||
+ " \"columns\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"columnName\": \"OrderID\",\n"
|
||||
+ " \"columnType\": \"bigint(20)\",\n"
|
||||
+ " \"primaryKey\": true,\n"
|
||||
+ " \"autoIncrement\": true,\n"
|
||||
+ " \"columnComment\": \"Order ID\"\n"
|
||||
+ " },\n"
|
||||
+ " {\n"
|
||||
+ " \"columnName\": \"Customer Name\",\n"
|
||||
+ " \"columnType\": \"varchar(64)\",\n"
|
||||
+ " \"columnComment\": \"Customer name\"\n"
|
||||
+ " },\n"
|
||||
+ " {\n"
|
||||
+ " \"columnName\": \"客户名称\",\n"
|
||||
+ " \"columnType\": \"varchar(64)\",\n"
|
||||
+ " \"columnComment\": \"Customer name cn\"\n"
|
||||
+ " },\n"
|
||||
+ " {\n"
|
||||
+ " \"columnName\": \"customer-name\",\n"
|
||||
+ " \"columnType\": \"varchar(64)\",\n"
|
||||
+ " \"columnComment\": \"Customer name duplicate\"\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String responseWithUnsupportedColumnTypes()
|
||||
{
|
||||
return "{\n"
|
||||
+ " \"tables\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"tableName\": \"order_info\",\n"
|
||||
+ " \"tableComment\": \"Order info\",\n"
|
||||
+ " \"columns\": [\n"
|
||||
+ " {\"columnName\": \"order_id\", \"columnType\": \"BIGINT unsigned\", \"primaryKey\": true, \"autoIncrement\": true, \"columnComment\": \"Order ID\"},\n"
|
||||
+ " {\"columnName\": \"order_status\", \"columnType\": \"INTEGER\", \"columnComment\": \"Order status\"},\n"
|
||||
+ " {\"columnName\": \"customer_name\", \"columnType\": \"string\", \"columnComment\": \"Customer name\"},\n"
|
||||
+ " {\"columnName\": \"enabled\", \"columnType\": \"boolean\", \"columnComment\": \"Enabled\"},\n"
|
||||
+ " {\"columnName\": \"created_at\", \"columnType\": \"timestamp\", \"columnComment\": \"Created time\"},\n"
|
||||
+ " {\"columnName\": \"extra_data\", \"columnType\": \"json\", \"columnComment\": \"Extra data\"},\n"
|
||||
+ " {\"columnName\": \"amount\", \"columnType\": \"float\", \"columnComment\": \"Amount\"},\n"
|
||||
+ " {\"columnName\": \"remark\", \"columnType\": \"varchar(5000) not null\", \"columnComment\": \"Remark\"}\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,22 @@ import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -51,6 +60,27 @@ public class FrontProjectPreviewServiceImplTest
|
||||
assertEquals("backend", structure.get(0).get("label"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadAllPackagesBackendFrontendAndAdminFrontend() throws Exception
|
||||
{
|
||||
FrontProject project = frontProjectWithOneTable();
|
||||
project.setPreviewStatus("1");
|
||||
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
|
||||
when(genProjectService.downloadStructure(any(GenProject.class), eq("backend")))
|
||||
.thenReturn(zipWithEntry("demo/backend.txt", "backend"));
|
||||
when(genProjectService.downloadStructure(any(GenProject.class), eq("frontend")))
|
||||
.thenReturn(zipWithEntry("demo/frontend.txt", "frontend"));
|
||||
when(genProjectService.downloadStructure(any(GenProject.class), eq("admin_frontend")))
|
||||
.thenReturn(zipWithEntry("demo/admin.txt", "admin"));
|
||||
|
||||
byte[] data = previewService.downloadAll(100L, 200L);
|
||||
|
||||
Set<String> entries = zipEntries(data);
|
||||
assertTrue(entries.contains("backend/demo/backend.txt"));
|
||||
assertTrue(entries.contains("front-web/demo/frontend.txt"));
|
||||
assertTrue(entries.contains("admin-web/demo/admin.txt"));
|
||||
}
|
||||
|
||||
private void setField(String name, Object value) throws Exception
|
||||
{
|
||||
Field field = FrontProjectPreviewServiceImpl.class.getDeclaredField(name);
|
||||
@@ -89,4 +119,29 @@ public class FrontProjectPreviewServiceImplTest
|
||||
column.setIsPk("1");
|
||||
return column;
|
||||
}
|
||||
|
||||
private byte[] zipWithEntry(String name, String content) throws IOException
|
||||
{
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ZipOutputStream zip = new ZipOutputStream(outputStream);
|
||||
zip.putNextEntry(new ZipEntry(name));
|
||||
zip.write(content.getBytes("UTF-8"));
|
||||
zip.closeEntry();
|
||||
zip.close();
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
private Set<String> zipEntries(byte[] data) throws IOException
|
||||
{
|
||||
Set<String> entries = new HashSet<String>();
|
||||
ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(data));
|
||||
ZipEntry entry;
|
||||
while ((entry = zip.getNextEntry()) != null)
|
||||
{
|
||||
entries.add(entry.getName());
|
||||
zip.closeEntry();
|
||||
}
|
||||
zip.close();
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import static org.mockito.Mockito.when;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -156,6 +157,7 @@ public class FrontProjectServiceImplTest
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.emptyList());
|
||||
DatabaseDesignResponse request = database(table("sys_user", pkColumn(), column("user_name", "varchar(64)", "0")));
|
||||
request.setErDiagram(Collections.singletonMap("relations", Collections.singletonList("student_department")));
|
||||
|
||||
service.saveDatabase(7L, 10L, request);
|
||||
|
||||
@@ -166,6 +168,27 @@ public class FrontProjectServiceImplTest
|
||||
assertEquals(Long.valueOf(10L), tableCaptor.getValue().getProjectId());
|
||||
verify(frontProjectColumnMapper).deleteColumnsByProjectId(10L);
|
||||
verify(frontProjectTableMapper).deleteTablesByProjectId(10L);
|
||||
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
||||
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
|
||||
assertEquals("1", projectCaptor.getValue().getGenerateStatus());
|
||||
assertEquals("0", projectCaptor.getValue().getPreviewStatus());
|
||||
assertTrue(projectCaptor.getValue().getErDiagram().contains("student_department"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDatabaseReturnsErDiagramDraft()
|
||||
{
|
||||
FrontProject project = project();
|
||||
project.setErDiagram("{\"positions\":{\"department\":{\"x\":24,\"y\":48}}}");
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project);
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.emptyList());
|
||||
|
||||
DatabaseDesignResponse response = service.getDatabase(7L, 10L);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> erDiagram = (Map<String, Object>) response.getErDiagram();
|
||||
assertTrue(erDiagram.containsKey("positions"));
|
||||
}
|
||||
|
||||
private void setField(String name, Object value) throws Exception
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import org.junit.Test;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import com.ruoyi.generator.config.DeepSeekProperties;
|
||||
|
||||
public class HttpDeepSeekClientTest
|
||||
{
|
||||
@Test
|
||||
public void requestFactoryUsesDefaultSslVerificationByDefault() throws Exception
|
||||
{
|
||||
HttpDeepSeekClient client = client(false);
|
||||
|
||||
SimpleClientHttpRequestFactory factory = client.requestFactory(1000);
|
||||
|
||||
assertSame(SimpleClientHttpRequestFactory.class, factory.getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestFactoryCanSkipSslVerificationWhenEnabled() throws Exception
|
||||
{
|
||||
HttpDeepSeekClient client = client(true);
|
||||
|
||||
SimpleClientHttpRequestFactory factory = client.requestFactory(1000);
|
||||
|
||||
assertNotEquals(SimpleClientHttpRequestFactory.class, factory.getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipSslVerifyDefaultsToFalse()
|
||||
{
|
||||
DeepSeekProperties properties = new DeepSeekProperties();
|
||||
|
||||
assertFalse(properties.isSkipSslVerify());
|
||||
properties.setSkipSslVerify(true);
|
||||
assertTrue(properties.isSkipSslVerify());
|
||||
properties.setSkipSslVerify(null);
|
||||
assertFalse(properties.isSkipSslVerify());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deepSeekTimeoutDefaultsToLongRunningGeneration()
|
||||
{
|
||||
assertEquals(Integer.valueOf(180000), new DeepSeekProperties().getTimeout());
|
||||
}
|
||||
|
||||
private HttpDeepSeekClient client(boolean skipSslVerify) throws Exception
|
||||
{
|
||||
HttpDeepSeekClient client = new HttpDeepSeekClient();
|
||||
DeepSeekProperties properties = new DeepSeekProperties();
|
||||
properties.setSkipSslVerify(skipSslVerify);
|
||||
setField(client, "deepSeekProperties", properties);
|
||||
return client;
|
||||
}
|
||||
|
||||
private void setField(Object target, String name, Object value) throws Exception
|
||||
{
|
||||
Field field = target.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
}
|
||||
47
RuoYi-Vue/ruoyi-ui/src/api/generator/sourceProject.js
Normal file
47
RuoYi-Vue/ruoyi-ui/src/api/generator/sourceProject.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listSourceProject(query) {
|
||||
return request({
|
||||
url: '/generator/sourceProject/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getSourceProject(projectId) {
|
||||
return request({
|
||||
url: '/generator/sourceProject/' + projectId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addSourceProject(data) {
|
||||
return request({
|
||||
url: '/generator/sourceProject',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateSourceProject(data) {
|
||||
return request({
|
||||
url: '/generator/sourceProject',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function changeSourceProjectStatus(projectId, status) {
|
||||
return request({
|
||||
url: '/generator/sourceProject/changeStatus',
|
||||
method: 'put',
|
||||
data: { projectId, status }
|
||||
})
|
||||
}
|
||||
|
||||
export function delSourceProject(projectId) {
|
||||
return request({
|
||||
url: '/generator/sourceProject/' + projectId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
24
RuoYi-Vue/ruoyi-ui/src/api/generator/sourcePurchase.js
Normal file
24
RuoYi-Vue/ruoyi-ui/src/api/generator/sourcePurchase.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listSourcePurchase(query) {
|
||||
return request({
|
||||
url: '/generator/sourcePurchase/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getSourcePurchase(purchaseId) {
|
||||
return request({
|
||||
url: '/generator/sourcePurchase/' + purchaseId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function updateSourcePurchase(data) {
|
||||
return request({
|
||||
url: '/generator/sourcePurchase',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
360
RuoYi-Vue/ruoyi-ui/src/views/generator/sourceProject/index.vue
Normal file
360
RuoYi-Vue/ruoyi-ui/src/views/generator/sourceProject/index.vue
Normal file
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="80px">
|
||||
<el-form-item label="项目名称" prop="projectName">
|
||||
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable size="small" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类" prop="category">
|
||||
<el-select v-model="queryParams.category" placeholder="请选择分类" clearable size="small">
|
||||
<el-option v-for="dict in dict.type.source_project_category" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
|
||||
<el-option label="上架" value="0" />
|
||||
<el-option label="下架" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['generator:sourceProject:add']">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['generator:sourceProject:edit']">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['generator:sourceProject:remove']">删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['generator:sourceProject:export']">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="sourceProjectList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="项目名称" align="center" prop="projectName" min-width="180" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="标识" align="center" prop="slug" min-width="140" />
|
||||
<el-table-column label="分类" align="center" prop="category" width="120">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.source_project_category" :value="scope.row.category" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" align="center" prop="price" width="100">
|
||||
<template slot-scope="scope">{{ formatPrice(scope.row.price) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="浏览量" align="center" prop="viewCount" width="100" />
|
||||
<el-table-column label="排序" align="center" prop="sort" width="80" />
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.status"
|
||||
active-value="0"
|
||||
inactive-value="1"
|
||||
active-text="上架"
|
||||
inactive-text="下架"
|
||||
@change="handleStatusChange(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="170">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['generator:sourceProject:edit']">修改</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['generator:sourceProject:remove']">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<el-dialog :title="title" :visible.sync="open" width="760px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="96px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目名称" prop="projectName">
|
||||
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="访问标识" prop="slug">
|
||||
<el-input v-model="form.slug" placeholder="例如 dms-platform" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="分类" prop="category">
|
||||
<el-select v-model="form.category" placeholder="请选择分类" style="width: 100%">
|
||||
<el-option v-for="dict in dict.type.source_project_category" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="封面标识" prop="coverText">
|
||||
<el-input v-model="form.coverText" placeholder="例如 DMS" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="价格(元)" prop="priceYuan">
|
||||
<el-input-number v-model="form.priceYuan" :min="0" :precision="2" :step="10" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="项目摘要" prop="summary">
|
||||
<el-input v-model="form.summary" type="textarea" :rows="2" placeholder="请输入项目摘要" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目描述" prop="descriptionMd">
|
||||
<el-input v-model="form.descriptionMd" type="textarea" :rows="8" placeholder="支持 Markdown" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="技术标签">
|
||||
<el-input v-model="form.tagsText" type="textarea" :rows="4" placeholder="一行一个标签" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="功能模块">
|
||||
<el-input v-model="form.modulesText" type="textarea" :rows="4" placeholder="一行一个模块" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="适用场景">
|
||||
<el-input v-model="form.scenesText" type="textarea" :rows="4" placeholder="一行一个场景" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资源名称">
|
||||
<el-input v-model="form.resourceName" placeholder="源码包名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资源地址">
|
||||
<el-input v-model="form.resourceUrl" placeholder="下载地址或对象存储地址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="资源版本">
|
||||
<el-input v-model="form.resourceVersion" placeholder="例如 1.0.0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="资源大小">
|
||||
<el-input v-model="form.resourceSize" placeholder="例如 86 MB" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio label="0">上架</el-radio>
|
||||
<el-radio label="1">下架</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addSourceProject, changeSourceProjectStatus, delSourceProject, getSourceProject, listSourceProject, updateSourceProject } from '@/api/generator/sourceProject'
|
||||
|
||||
export default {
|
||||
name: 'SourceProject',
|
||||
dicts: ['source_project_category'],
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
ids: [],
|
||||
single: true,
|
||||
multiple: true,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
sourceProjectList: [],
|
||||
open: false,
|
||||
title: '',
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
projectName: null,
|
||||
category: null,
|
||||
status: null
|
||||
},
|
||||
form: {},
|
||||
rules: {
|
||||
projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
|
||||
slug: [{ required: true, message: '访问标识不能为空', trigger: 'blur' }],
|
||||
category: [{ required: true, message: '分类不能为空', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.loading = true
|
||||
listSourceProject(this.queryParams).then(response => {
|
||||
this.sourceProjectList = response.rows
|
||||
this.total = response.total
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
this.open = false
|
||||
this.reset()
|
||||
},
|
||||
reset() {
|
||||
this.form = {
|
||||
projectId: null,
|
||||
slug: '',
|
||||
projectName: '',
|
||||
category: this.defaultCategory(),
|
||||
coverText: '',
|
||||
summary: '',
|
||||
descriptionMd: '',
|
||||
priceYuan: 0,
|
||||
viewCount: 0,
|
||||
tagsText: '',
|
||||
modulesText: '',
|
||||
scenesText: '',
|
||||
resourceName: '',
|
||||
resourceUrl: '',
|
||||
resourceVersion: '1.0.0',
|
||||
resourceSize: '',
|
||||
sort: 0,
|
||||
status: '0',
|
||||
remark: ''
|
||||
}
|
||||
this.resetForm('form')
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
resetQuery() {
|
||||
this.resetForm('queryForm')
|
||||
this.handleQuery()
|
||||
},
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.projectId)
|
||||
this.single = selection.length !== 1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
handleAdd() {
|
||||
this.reset()
|
||||
this.open = true
|
||||
this.title = '新增源码项目'
|
||||
},
|
||||
handleUpdate(row) {
|
||||
this.reset()
|
||||
const projectId = row.projectId || this.ids
|
||||
getSourceProject(projectId).then(response => {
|
||||
this.form = this.toForm(response.data)
|
||||
this.open = true
|
||||
this.title = '修改源码项目'
|
||||
})
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return
|
||||
const data = this.toPayload(this.form)
|
||||
const request = data.projectId != null ? updateSourceProject(data) : addSourceProject(data)
|
||||
request.then(() => {
|
||||
this.$modal.msgSuccess(data.projectId != null ? '修改成功' : '新增成功')
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleStatusChange(row) {
|
||||
const text = row.status === '0' ? '上架' : '下架'
|
||||
this.$modal.confirm(`确认要${text}「${row.projectName}」吗?`).then(() => {
|
||||
return changeSourceProjectStatus(row.projectId, row.status)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess(`${text}成功`)
|
||||
}).catch(() => {
|
||||
row.status = row.status === '0' ? '1' : '0'
|
||||
})
|
||||
},
|
||||
handleDelete(row) {
|
||||
const projectIds = row.projectId || this.ids
|
||||
this.$modal.confirm('是否确认删除源码项目编号为 "' + projectIds + '" 的数据项?').then(function() {
|
||||
return delSourceProject(projectIds)
|
||||
}).then(() => {
|
||||
this.getList()
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleExport() {
|
||||
this.download('generator/sourceProject/export', {
|
||||
...this.queryParams
|
||||
}, `source_project_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
toForm(data) {
|
||||
return {
|
||||
...data,
|
||||
priceYuan: this.centsToYuan(data.price),
|
||||
tagsText: this.toLines(data.tags),
|
||||
modulesText: this.toLines(data.modules),
|
||||
scenesText: this.toLines(data.scenes)
|
||||
}
|
||||
},
|
||||
toPayload(form) {
|
||||
return {
|
||||
...form,
|
||||
price: this.yuanToCents(form.priceYuan),
|
||||
tags: this.fromLines(form.tagsText),
|
||||
modules: this.fromLines(form.modulesText),
|
||||
scenes: this.fromLines(form.scenesText)
|
||||
}
|
||||
},
|
||||
toLines(value) {
|
||||
if (!value) return ''
|
||||
try {
|
||||
const parsed = Array.isArray(value) ? value : JSON.parse(value)
|
||||
return Array.isArray(parsed) ? parsed.join('\n') : ''
|
||||
} catch (error) {
|
||||
return value
|
||||
}
|
||||
},
|
||||
fromLines(value) {
|
||||
return JSON.stringify(String(value || '').split(/[,,\n]/).map(item => item.trim()).filter(Boolean))
|
||||
},
|
||||
centsToYuan(value) {
|
||||
return Number(value || 0) / 100
|
||||
},
|
||||
yuanToCents(value) {
|
||||
return Math.round(Number(value || 0) * 100)
|
||||
},
|
||||
formatPrice(value) {
|
||||
const amount = this.centsToYuan(value)
|
||||
return `¥${Number.isInteger(amount) ? amount : amount.toFixed(2)}`
|
||||
},
|
||||
defaultCategory() {
|
||||
const categories = this.dict.type.source_project_category || []
|
||||
return categories.length ? categories[0].value : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
194
RuoYi-Vue/ruoyi-ui/src/views/generator/sourcePurchase/index.vue
Normal file
194
RuoYi-Vue/ruoyi-ui/src/views/generator/sourcePurchase/index.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="80px">
|
||||
<el-form-item label="订单号" prop="orderNo">
|
||||
<el-input v-model="queryParams.orderNo" placeholder="请输入订单号" clearable size="small" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称" prop="projectName">
|
||||
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable size="small" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="支付状态" prop="payStatus">
|
||||
<el-select v-model="queryParams.payStatus" placeholder="请选择支付状态" clearable size="small">
|
||||
<el-option label="待支付" value="0" />
|
||||
<el-option label="已支付" value="1" />
|
||||
<el-option label="已取消" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="资源状态" prop="resourceStatus">
|
||||
<el-select v-model="queryParams.resourceStatus" placeholder="请选择资源状态" clearable size="small">
|
||||
<el-option label="未发放" value="0" />
|
||||
<el-option label="已发放" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['generator:sourcePurchase:edit']">修改状态</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['generator:sourcePurchase:export']">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="sourcePurchaseList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="订单号" align="center" prop="orderNo" min-width="190" />
|
||||
<el-table-column label="项目名称" align="center" prop="projectName" min-width="180" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="用户ID" align="center" prop="userId" width="90" />
|
||||
<el-table-column label="金额" align="center" prop="amount" width="100">
|
||||
<template slot-scope="scope">{{ formatPrice(scope.row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付状态" align="center" prop="payStatus" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="payStatusType(scope.row.payStatus)">{{ payStatusLabel(scope.row.payStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="资源状态" align="center" prop="resourceStatus" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.resourceStatus === '1' ? 'success' : 'info'">{{ scope.row.resourceStatus === '1' ? '已发放' : '未发放' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="购买时间" align="center" prop="purchaseTime" width="160" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['generator:sourcePurchase:edit']">修改</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<el-dialog :title="title" :visible.sync="open" width="460px" append-to-body>
|
||||
<el-form ref="form" :model="form" label-width="90px">
|
||||
<el-form-item label="订单号">
|
||||
<el-input v-model="form.orderNo" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="支付状态">
|
||||
<el-radio-group v-model="form.payStatus">
|
||||
<el-radio label="0">待支付</el-radio>
|
||||
<el-radio label="1">已支付</el-radio>
|
||||
<el-radio label="2">已取消</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="资源状态">
|
||||
<el-radio-group v-model="form.resourceStatus">
|
||||
<el-radio label="0">未发放</el-radio>
|
||||
<el-radio label="1">已发放</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSourcePurchase, listSourcePurchase, updateSourcePurchase } from '@/api/generator/sourcePurchase'
|
||||
|
||||
export default {
|
||||
name: 'SourcePurchase',
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
ids: [],
|
||||
single: true,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
sourcePurchaseList: [],
|
||||
open: false,
|
||||
title: '',
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
orderNo: null,
|
||||
projectName: null,
|
||||
payStatus: null,
|
||||
resourceStatus: null
|
||||
},
|
||||
form: {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.loading = true
|
||||
listSourcePurchase(this.queryParams).then(response => {
|
||||
this.sourcePurchaseList = response.rows
|
||||
this.total = response.total
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
this.open = false
|
||||
this.reset()
|
||||
},
|
||||
reset() {
|
||||
this.form = {
|
||||
purchaseId: null,
|
||||
orderNo: '',
|
||||
payStatus: '0',
|
||||
resourceStatus: '0',
|
||||
remark: ''
|
||||
}
|
||||
this.resetForm('form')
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
resetQuery() {
|
||||
this.resetForm('queryForm')
|
||||
this.handleQuery()
|
||||
},
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.purchaseId)
|
||||
this.single = selection.length !== 1
|
||||
},
|
||||
handleUpdate(row) {
|
||||
this.reset()
|
||||
const purchaseId = row.purchaseId || this.ids
|
||||
getSourcePurchase(purchaseId).then(response => {
|
||||
this.form = response.data
|
||||
this.open = true
|
||||
this.title = '修改购买记录'
|
||||
})
|
||||
},
|
||||
submitForm() {
|
||||
updateSourcePurchase(this.form).then(() => {
|
||||
this.$modal.msgSuccess('修改成功')
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
handleExport() {
|
||||
this.download('generator/sourcePurchase/export', {
|
||||
...this.queryParams
|
||||
}, `source_purchase_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
formatPrice(value) {
|
||||
const amount = Number(value || 0) / 100
|
||||
return `¥${Number.isInteger(amount) ? amount : amount.toFixed(2)}`
|
||||
},
|
||||
payStatusLabel(value) {
|
||||
return { '0': '待支付', '1': '已支付', '2': '已取消' }[value] || value
|
||||
},
|
||||
payStatusType(value) {
|
||||
return { '0': 'warning', '1': 'success', '2': 'info' }[value] || 'info'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -201,6 +201,8 @@ insert into sys_menu values('121', '模板管理', '118', '3', 'template', 'g
|
||||
insert into sys_menu values('122', '项目结构', '118', '4', 'structure', 'generator/structure/index', '', '', 1, 0, 'C', '0', '0', 'generator:structure:list', 'tree', 'admin', sysdate(), '', null, '项目结构菜单');
|
||||
insert into sys_menu values('123', '功能模块', '118', '5', 'module', 'generator/module/index', '', '', 1, 0, 'C', '0', '0', 'generator:module:list', 'tree-table', 'admin', sysdate(), '', null, '功能模块菜单');
|
||||
insert into sys_menu values('124', '代码片段', '118', '6', 'snippet', 'generator/snippet/index', '', '', 1, 0, 'C', '0', '0', 'generator:snippet:list', 'list', 'admin', sysdate(), '', null, '代码片段菜单');
|
||||
insert into sys_menu values('125', '源码库管理', '118', '7', 'sourceProject', 'generator/sourceProject/index', '', '', 1, 0, 'C', '0', '0', 'generator:sourceProject:list', 'code', 'admin', sysdate(), '', null, '源码库管理菜单');
|
||||
insert into sys_menu values('126', '源码购买记录', '118', '8', 'sourcePurchase', 'generator/sourcePurchase/index', '', '', 1, 0, 'C', '0', '0', 'generator:sourcePurchase:list', 'money', 'admin', sysdate(), '', null, '源码购买记录菜单');
|
||||
-- 三级菜单
|
||||
insert into sys_menu values('500', '操作日志', '108', '1', 'operlog', 'monitor/operlog/index', '', '', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 'admin', sysdate(), '', null, '操作日志菜单');
|
||||
insert into sys_menu values('501', '登录日志', '108', '2', 'logininfor', 'monitor/logininfor/index', '', '', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'logininfor', 'admin', sysdate(), '', null, '登录日志菜单');
|
||||
@@ -324,6 +326,16 @@ insert into sys_menu values('1097', '片段新增', '124', '2', '#', '', '', '',
|
||||
insert into sys_menu values('1098', '片段修改', '124', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:snippet:edit', '#', 'admin', sysdate(), '', null, '');
|
||||
insert into sys_menu values('1099', '片段删除', '124', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:snippet:remove', '#', 'admin', sysdate(), '', null, '');
|
||||
insert into sys_menu values('1100', '片段导出', '124', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:snippet:export', '#', 'admin', sysdate(), '', null, '');
|
||||
-- 代码生成平台-源码库管理按钮
|
||||
insert into sys_menu values('1101', '源码查询', '125', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:sourceProject:query', '#', 'admin', sysdate(), '', null, '');
|
||||
insert into sys_menu values('1102', '源码新增', '125', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:sourceProject:add', '#', 'admin', sysdate(), '', null, '');
|
||||
insert into sys_menu values('1103', '源码修改', '125', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:sourceProject:edit', '#', 'admin', sysdate(), '', null, '');
|
||||
insert into sys_menu values('1104', '源码删除', '125', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:sourceProject:remove', '#', 'admin', sysdate(), '', null, '');
|
||||
insert into sys_menu values('1105', '源码导出', '125', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:sourceProject:export', '#', 'admin', sysdate(), '', null, '');
|
||||
-- 代码生成平台-源码购买记录按钮
|
||||
insert into sys_menu values('1106', '购买查询', '126', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:sourcePurchase:query', '#', 'admin', sysdate(), '', null, '');
|
||||
insert into sys_menu values('1107', '购买修改', '126', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:sourcePurchase:edit', '#', 'admin', sysdate(), '', null, '');
|
||||
insert into sys_menu values('1108', '购买导出', '126', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'generator:sourcePurchase:export', '#', 'admin', sysdate(), '', null, '');
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
@@ -583,6 +595,7 @@ insert into sys_dict_type values(7, '通知类型', 'sys_notice_type', '0',
|
||||
insert into sys_dict_type values(8, '通知状态', 'sys_notice_status', '0', 'admin', sysdate(), '', null, '通知状态列表');
|
||||
insert into sys_dict_type values(9, '操作类型', 'sys_oper_type', '0', 'admin', sysdate(), '', null, '操作类型列表');
|
||||
insert into sys_dict_type values(10, '系统状态', 'sys_common_status', '0', 'admin', sysdate(), '', null, '登录状态列表');
|
||||
insert into sys_dict_type values(11, '源码分类', 'source_project_category', '0', 'admin', sysdate(), '', null, '源码库分类列表');
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
@@ -637,6 +650,11 @@ insert into sys_dict_data values(26, 8, '生成代码', '8', 'sys_oper_ty
|
||||
insert into sys_dict_data values(27, 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '清空操作');
|
||||
insert into sys_dict_data values(28, 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '正常状态');
|
||||
insert into sys_dict_data values(29, 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态');
|
||||
insert into sys_dict_data values(30, 1, '企业管理系统', 'enterprise', 'source_project_category', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '源码分类-企业管理系统');
|
||||
insert into sys_dict_data values(31, 2, '商城系统', 'mall', 'source_project_category', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '源码分类-商城系统');
|
||||
insert into sys_dict_data values(32, 3, 'OA 办公', 'office', 'source_project_category', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '源码分类-OA 办公');
|
||||
insert into sys_dict_data values(33, 4, '低代码平台', 'lowcode', 'source_project_category', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '源码分类-低代码平台');
|
||||
insert into sys_dict_data values(34, 5, '微服务脚手架', 'microservice', 'source_project_category', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '源码分类-微服务脚手架');
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
@@ -1281,6 +1299,7 @@ create table front_project (
|
||||
back_framework varchar(50) default 'Spring Boot + MyBatis' comment '后端框架',
|
||||
generate_status char(1) default '0' comment '数据库生成状态(0未生成 1生成中 2已生成 3失败)',
|
||||
preview_status char(1) default '0' comment '预览状态(0未预览 1已预览 2失败)',
|
||||
er_diagram longtext comment 'ER图草稿JSON',
|
||||
status char(1) default '0' comment '状态(0正常 1停用)',
|
||||
create_time datetime default null comment '创建时间',
|
||||
update_time datetime default null comment '更新时间',
|
||||
@@ -1378,4 +1397,86 @@ create table front_project_file (
|
||||
key idx_front_project_file_lookup (project_id, template_type, table_id)
|
||||
) engine=innodb auto_increment=100 default charset=utf8mb4 comment='前台项目预览文件缓存';
|
||||
|
||||
-- =================================================
|
||||
-- EasyCode source store tables
|
||||
-- =================================================
|
||||
|
||||
drop table if exists source_purchase;
|
||||
drop table if exists source_project;
|
||||
|
||||
create table source_project (
|
||||
project_id bigint(20) not null auto_increment comment '源码项目ID',
|
||||
slug varchar(100) not null comment '前台访问标识',
|
||||
project_name varchar(120) not null comment '项目名称',
|
||||
category varchar(50) default '' comment '分类',
|
||||
cover_text varchar(30) default '' comment '封面标识',
|
||||
summary varchar(500) default '' comment '项目摘要',
|
||||
description_md longtext comment 'Markdown项目描述',
|
||||
price bigint(20) default 0 comment '价格,单位分',
|
||||
view_count bigint(20) default 0 comment '浏览量',
|
||||
tags longtext comment '技术标签JSON',
|
||||
modules longtext comment '功能模块JSON',
|
||||
scenes longtext comment '适用场景JSON',
|
||||
resource_name varchar(200) default '' comment '源码资源名称',
|
||||
resource_url varchar(500) default '' comment '源码资源地址',
|
||||
resource_version varchar(50) default '' comment '源码资源版本',
|
||||
resource_size varchar(50) default '' comment '源码资源大小',
|
||||
sort int default 0 comment '排序',
|
||||
status char(1) default '0' comment '状态(0上架 1下架)',
|
||||
del_flag char(1) default '0' comment '删除标志(0存在 2删除)',
|
||||
create_by varchar(64) default '' comment '创建者',
|
||||
create_time datetime default null comment '创建时间',
|
||||
update_by varchar(64) default '' comment '更新者',
|
||||
update_time datetime default null comment '更新时间',
|
||||
remark varchar(500) default null comment '备注',
|
||||
primary key (project_id),
|
||||
unique key uk_source_project_slug (slug),
|
||||
key idx_source_project_category (category),
|
||||
key idx_source_project_status (status, del_flag)
|
||||
) engine=innodb auto_increment=100 default charset=utf8mb4 comment='源码库项目表';
|
||||
|
||||
create table source_purchase (
|
||||
purchase_id bigint(20) not null auto_increment comment '购买记录ID',
|
||||
project_id bigint(20) not null comment '源码项目ID',
|
||||
user_id bigint(20) not null comment '前台用户ID',
|
||||
order_no varchar(64) not null comment '订单号',
|
||||
amount bigint(20) default 0 comment '金额,单位分',
|
||||
pay_status char(1) default '0' comment '支付状态(0待支付 1已支付 2已取消)',
|
||||
resource_status char(1) default '0' comment '资源状态(0未发放 1已发放)',
|
||||
purchase_time datetime default null comment '购买时间',
|
||||
create_by varchar(64) default '' comment '创建者',
|
||||
create_time datetime default null comment '创建时间',
|
||||
update_by varchar(64) default '' comment '更新者',
|
||||
update_time datetime default null comment '更新时间',
|
||||
remark varchar(500) default null comment '备注',
|
||||
primary key (purchase_id),
|
||||
unique key uk_source_purchase_order_no (order_no),
|
||||
key idx_source_purchase_user (user_id),
|
||||
key idx_source_purchase_project (project_id),
|
||||
constraint fk_source_purchase_project foreign key (project_id) references source_project(project_id),
|
||||
constraint fk_source_purchase_user foreign key (user_id) references front_user(user_id)
|
||||
) engine=innodb auto_increment=100 default charset=utf8mb4 comment='源码购买记录表';
|
||||
|
||||
insert into source_project
|
||||
(project_id, slug, project_name, category, cover_text, summary, description_md, price, view_count, tags, modules, scenes, resource_name, resource_url, resource_version, resource_size, sort, status, del_flag, create_by, create_time, update_by, update_time, remark)
|
||||
values
|
||||
(100, 'dms-platform', '商用车 DMS 管理系统', 'enterprise', 'DMS', '覆盖经销商、维修工单、索赔、配件库存、服务活动和结算的商用车售后管理源码。',
|
||||
'# 商用车 DMS 管理系统\n\n这套源码面向商用车售后服务场景,提供经销商、车辆档案、维修工单、索赔流程、配件库存和结算对账等核心能力。\n\n## 核心功能\n\n- 经销商档案、服务站和维修技师管理\n- 车辆档案、保养提醒和维修历史追踪\n- 维修工单、派工、完工质检和费用结算\n- 配件入库、出库、调拨和库存预警\n- 索赔申请、审核、结算和统计报表', 39900, 2860,
|
||||
'["Spring Boot","Vue2","MySQL"]', '["经销商管理","维修工单","配件库存","索赔结算","服务活动"]', '["汽车后市场","商用车售后","经销商协同"]', '', '', '1.0.0', '', 1, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null),
|
||||
(101, 'mall-order-system', '商城订单管理系统', 'mall', 'MALL', '包含商品、订单、支付、库存、优惠券、会员和后台统计模块的商城管理源码。',
|
||||
'# 商城订单管理系统\n\n项目提供从商品上架到订单履约的完整后台管理能力,适合作为中小型商城或订单中台的启动源码。\n\n## 功能模块\n\n- 商品分类、SKU、库存和上下架\n- 购物车、订单、支付记录和售后单\n- 优惠券、满减活动和会员等级\n- 后台统计看板和经营数据导出', 29900, 1980,
|
||||
'["Spring Cloud","Redis","Vue3"]', '["商品中心","订单中心","支付记录","会员体系","营销活动"]', '["电商后台","订单运营","会员营销"]', '', '', '1.0.0', '', 2, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null),
|
||||
(102, 'oa-approval-suite', 'OA 办公审批系统', 'office', 'OA', '集成用户、角色、菜单、审批流、请假、报销、公告和文件管理的办公源码。',
|
||||
'# OA 办公审批系统\n\n这是一套偏企业内网办公的基础源码,重点覆盖审批、公告、文件与权限管理。\n\n## 亮点\n\n- 可配置审批节点和审批人\n- 请假、报销、外出等常见办公流程\n- 公告通知、附件上传和文件归档\n- 角色、菜单、按钮级权限控制', 26800, 1750,
|
||||
'["Sa-Token","Element UI","工作流"]', '["审批流程","请假报销","公告通知","文件管理","权限菜单"]', '["企业内网","行政审批","协同办公"]', '', '', '1.0.0', '', 3, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null),
|
||||
(103, 'erp-warehouse', 'ERP 仓储管理系统', 'enterprise', 'ERP', '提供采购、销售、入库、出库、盘点、供应商和库存报表模块的仓储源码。',
|
||||
'# ERP 仓储管理系统\n\n源码聚焦进销存和仓储管理,适合小型供应链、仓库和贸易公司快速搭建业务后台。\n\n## 模块清单\n\n- 采购订单、采购入库和供应商档案\n- 销售订单、销售出库和客户档案\n- 库存流水、库存盘点和预警规则\n- Excel 导入导出与库存报表', 19900, 1320,
|
||||
'["ERP","MyBatis-Plus","Excel"]', '["采购入库","销售出库","库存盘点","供应商管理","报表导出"]', '["仓储管理","进销存","供应链后台"]', '', '', '1.0.0', '', 4, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null),
|
||||
(104, 'lowcode-form-platform', '低代码表单平台', 'lowcode', 'FORM', '支持动态表单、拖拽布局、字段配置、数据字典和在线设计的低代码源码。',
|
||||
'# 低代码表单平台\n\n项目用于构建配置化表单和数据采集页面,包含设计器、字段配置、预览和提交数据管理。\n\n## 能力范围\n\n- 拖拽式表单组件编排\n- 字段属性、校验规则和选项配置\n- 字典数据联动\n- 表单预览与数据提交记录', 49900, 980,
|
||||
'["Vue3","Element Plus","拖拽设计"]', '["表单设计器","字段配置","动态校验","数据字典","表单预览"]', '["低代码平台","动态表单","配置化后台"]', '', '', '1.0.0', '', 5, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null),
|
||||
(105, 'microservice-auth-starter', '微服务权限脚手架', 'microservice', 'MS', '集成网关、认证中心、系统服务、日志服务、文件服务和监控基础能力的脚手架源码。',
|
||||
'# 微服务权限脚手架\n\n这套源码适合从单体后台升级到微服务架构的团队,预置认证、网关、系统权限和基础监控能力。\n\n## 服务组成\n\n- Gateway 网关统一鉴权和路由\n- Auth 认证中心\n- System 系统权限服务\n- Log 操作日志服务\n- File 文件服务', 59900, 2560,
|
||||
'["Spring Cloud","Nacos","Gateway"]', '["网关服务","认证中心","系统服务","日志中心","文件服务"]', '["微服务基础架构","权限平台","多服务治理"]', '', '', '1.0.0', '', 6, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
3
RuoYi-Vue/sql/front_er_diagram.sql
Normal file
3
RuoYi-Vue/sql/front_er_diagram.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Run once on existing front workbench databases to persist ER diagram drafts.
|
||||
alter table front_project
|
||||
add column er_diagram longtext comment 'ER diagram draft JSON' after preview_status;
|
||||
@@ -42,6 +42,7 @@ create table front_project (
|
||||
back_framework varchar(50) default 'Spring Boot + MyBatis' comment '后端框架',
|
||||
generate_status char(1) default '0' comment '数据库生成状态(0未生成 1生成中 2已生成 3失败)',
|
||||
preview_status char(1) default '0' comment '预览状态(0未预览 1已预览 2失败)',
|
||||
er_diagram longtext comment 'ER图草稿JSON',
|
||||
status char(1) default '0' comment '状态(0正常 1停用)',
|
||||
create_time datetime default null comment '创建时间',
|
||||
update_time datetime default null comment '更新时间',
|
||||
|
||||
89
RuoYi-Vue/sql/source_store.sql
Normal file
89
RuoYi-Vue/sql/source_store.sql
Normal file
@@ -0,0 +1,89 @@
|
||||
-- EasyCode source store tables
|
||||
-- Import after sql/db.sql when upgrading an existing database.
|
||||
|
||||
delete from sys_dict_data where dict_type = 'source_project_category';
|
||||
delete from sys_dict_type where dict_type = 'source_project_category';
|
||||
insert into sys_dict_type values(null, '源码分类', 'source_project_category', '0', 'admin', sysdate(), '', null, '源码库分类列表');
|
||||
insert into sys_dict_data values(null, 1, '企业管理系统', 'enterprise', 'source_project_category', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '源码分类-企业管理系统');
|
||||
insert into sys_dict_data values(null, 2, '商城系统', 'mall', 'source_project_category', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '源码分类-商城系统');
|
||||
insert into sys_dict_data values(null, 3, 'OA 办公', 'office', 'source_project_category', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '源码分类-OA 办公');
|
||||
insert into sys_dict_data values(null, 4, '低代码平台', 'lowcode', 'source_project_category', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '源码分类-低代码平台');
|
||||
insert into sys_dict_data values(null, 5, '微服务脚手架', 'microservice', 'source_project_category', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '源码分类-微服务脚手架');
|
||||
|
||||
drop table if exists source_purchase;
|
||||
drop table if exists source_project;
|
||||
|
||||
create table source_project (
|
||||
project_id bigint(20) not null auto_increment comment '源码项目ID',
|
||||
slug varchar(100) not null comment '前台访问标识',
|
||||
project_name varchar(120) not null comment '项目名称',
|
||||
category varchar(50) default '' comment '分类',
|
||||
cover_text varchar(30) default '' comment '封面标识',
|
||||
summary varchar(500) default '' comment '项目摘要',
|
||||
description_md longtext comment 'Markdown项目描述',
|
||||
price bigint(20) default 0 comment '价格,单位分',
|
||||
view_count bigint(20) default 0 comment '浏览量',
|
||||
tags longtext comment '技术标签JSON',
|
||||
modules longtext comment '功能模块JSON',
|
||||
scenes longtext comment '适用场景JSON',
|
||||
resource_name varchar(200) default '' comment '源码资源名称',
|
||||
resource_url varchar(500) default '' comment '源码资源地址',
|
||||
resource_version varchar(50) default '' comment '源码资源版本',
|
||||
resource_size varchar(50) default '' comment '源码资源大小',
|
||||
sort int default 0 comment '排序',
|
||||
status char(1) default '0' comment '状态(0上架 1下架)',
|
||||
del_flag char(1) default '0' comment '删除标志(0存在 2删除)',
|
||||
create_by varchar(64) default '' comment '创建者',
|
||||
create_time datetime default null comment '创建时间',
|
||||
update_by varchar(64) default '' comment '更新者',
|
||||
update_time datetime default null comment '更新时间',
|
||||
remark varchar(500) default null comment '备注',
|
||||
primary key (project_id),
|
||||
unique key uk_source_project_slug (slug),
|
||||
key idx_source_project_category (category),
|
||||
key idx_source_project_status (status, del_flag)
|
||||
) engine=innodb auto_increment=100 default charset=utf8mb4 comment='源码库项目表';
|
||||
|
||||
create table source_purchase (
|
||||
purchase_id bigint(20) not null auto_increment comment '购买记录ID',
|
||||
project_id bigint(20) not null comment '源码项目ID',
|
||||
user_id bigint(20) not null comment '前台用户ID',
|
||||
order_no varchar(64) not null comment '订单号',
|
||||
amount bigint(20) default 0 comment '金额,单位分',
|
||||
pay_status char(1) default '0' comment '支付状态(0待支付 1已支付 2已取消)',
|
||||
resource_status char(1) default '0' comment '资源状态(0未发放 1已发放)',
|
||||
purchase_time datetime default null comment '购买时间',
|
||||
create_by varchar(64) default '' comment '创建者',
|
||||
create_time datetime default null comment '创建时间',
|
||||
update_by varchar(64) default '' comment '更新者',
|
||||
update_time datetime default null comment '更新时间',
|
||||
remark varchar(500) default null comment '备注',
|
||||
primary key (purchase_id),
|
||||
unique key uk_source_purchase_order_no (order_no),
|
||||
key idx_source_purchase_user (user_id),
|
||||
key idx_source_purchase_project (project_id),
|
||||
constraint fk_source_purchase_project foreign key (project_id) references source_project(project_id),
|
||||
constraint fk_source_purchase_user foreign key (user_id) references front_user(user_id)
|
||||
) engine=innodb auto_increment=100 default charset=utf8mb4 comment='源码购买记录表';
|
||||
|
||||
insert into source_project
|
||||
(project_id, slug, project_name, category, cover_text, summary, description_md, price, view_count, tags, modules, scenes, resource_name, resource_url, resource_version, resource_size, sort, status, del_flag, create_by, create_time, update_by, update_time, remark)
|
||||
values
|
||||
(100, 'dms-platform', '商用车 DMS 管理系统', 'enterprise', 'DMS', '覆盖经销商、维修工单、索赔、配件库存、服务活动和结算的商用车售后管理源码。',
|
||||
'# 商用车 DMS 管理系统\n\n这套源码面向商用车售后服务场景,提供经销商、车辆档案、维修工单、索赔流程、配件库存和结算对账等核心能力。\n\n## 核心功能\n\n- 经销商档案、服务站和维修技师管理\n- 车辆档案、保养提醒和维修历史追踪\n- 维修工单、派工、完工质检和费用结算\n- 配件入库、出库、调拨和库存预警\n- 索赔申请、审核、结算和统计报表', 39900, 2860,
|
||||
'["Spring Boot","Vue2","MySQL"]', '["经销商管理","维修工单","配件库存","索赔结算","服务活动"]', '["汽车后市场","商用车售后","经销商协同"]', '', '', '1.0.0', '', 1, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null),
|
||||
(101, 'mall-order-system', '商城订单管理系统', 'mall', 'MALL', '包含商品、订单、支付、库存、优惠券、会员和后台统计模块的商城管理源码。',
|
||||
'# 商城订单管理系统\n\n项目提供从商品上架到订单履约的完整后台管理能力,适合作为中小型商城或订单中台的启动源码。\n\n## 功能模块\n\n- 商品分类、SKU、库存和上下架\n- 购物车、订单、支付记录和售后单\n- 优惠券、满减活动和会员等级\n- 后台统计看板和经营数据导出', 29900, 1980,
|
||||
'["Spring Cloud","Redis","Vue3"]', '["商品中心","订单中心","支付记录","会员体系","营销活动"]', '["电商后台","订单运营","会员营销"]', '', '', '1.0.0', '', 2, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null),
|
||||
(102, 'oa-approval-suite', 'OA 办公审批系统', 'office', 'OA', '集成用户、角色、菜单、审批流、请假、报销、公告和文件管理的办公源码。',
|
||||
'# OA 办公审批系统\n\n这是一套偏企业内网办公的基础源码,重点覆盖审批、公告、文件与权限管理。\n\n## 亮点\n\n- 可配置审批节点和审批人\n- 请假、报销、外出等常见办公流程\n- 公告通知、附件上传和文件归档\n- 角色、菜单、按钮级权限控制', 26800, 1750,
|
||||
'["Sa-Token","Element UI","工作流"]', '["审批流程","请假报销","公告通知","文件管理","权限菜单"]', '["企业内网","行政审批","协同办公"]', '', '', '1.0.0', '', 3, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null),
|
||||
(103, 'erp-warehouse', 'ERP 仓储管理系统', 'enterprise', 'ERP', '提供采购、销售、入库、出库、盘点、供应商和库存报表模块的仓储源码。',
|
||||
'# ERP 仓储管理系统\n\n源码聚焦进销存和仓储管理,适合小型供应链、仓库和贸易公司快速搭建业务后台。\n\n## 模块清单\n\n- 采购订单、采购入库和供应商档案\n- 销售订单、销售出库和客户档案\n- 库存流水、库存盘点和预警规则\n- Excel 导入导出与库存报表', 19900, 1320,
|
||||
'["ERP","MyBatis-Plus","Excel"]', '["采购入库","销售出库","库存盘点","供应商管理","报表导出"]', '["仓储管理","进销存","供应链后台"]', '', '', '1.0.0', '', 4, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null),
|
||||
(104, 'lowcode-form-platform', '低代码表单平台', 'lowcode', 'FORM', '支持动态表单、拖拽布局、字段配置、数据字典和在线设计的低代码源码。',
|
||||
'# 低代码表单平台\n\n项目用于构建配置化表单和数据采集页面,包含设计器、字段配置、预览和提交数据管理。\n\n## 能力范围\n\n- 拖拽式表单组件编排\n- 字段属性、校验规则和选项配置\n- 字典数据联动\n- 表单预览与数据提交记录', 49900, 980,
|
||||
'["Vue3","Element Plus","拖拽设计"]', '["表单设计器","字段配置","动态校验","数据字典","表单预览"]', '["低代码平台","动态表单","配置化后台"]', '', '', '1.0.0', '', 5, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null),
|
||||
(105, 'microservice-auth-starter', '微服务权限脚手架', 'microservice', 'MS', '集成网关、认证中心、系统服务、日志服务、文件服务和监控基础能力的脚手架源码。',
|
||||
'# 微服务权限脚手架\n\n这套源码适合从单体后台升级到微服务架构的团队,预置认证、网关、系统权限和基础监控能力。\n\n## 服务组成\n\n- Gateway 网关统一鉴权和路由\n- Auth 认证中心\n- System 系统权限服务\n- Log 操作日志服务\n- File 文件服务', 59900, 2560,
|
||||
'["Spring Cloud","Nacos","Gateway"]', '["网关服务","认证中心","系统服务","日志中心","文件服务"]', '["微服务基础架构","权限平台","多服务治理"]', '', '', '1.0.0', '', 6, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null);
|
||||
Reference in New Issue
Block a user