直播笔记
快来分享你的内容吧~
- 2024-09-24·后端开发
- 2024-09-19·Java后端回顾并梳理学习项目过程和自己的实现步骤以及搭建、测试、学习等记录。 适当扩展内容: 1. 用户可以申请更换签名 sk [后端提供接口] 2. 网关通过 Dubbo RPC 校验是否还有调用次数 3. 网关优化 - 增加 [限流/降级保护机制] 提高性能, 整合Sentinel 组件整合 Nacos 完成 4. 引入分布式事务, 后台项目整合 Seata 组件 5. TODO:Docker 部署查看全文编程导航_小y:点赞 👍🏻 不过博客好像访问很慢:https://reurl.cc/GpabVW16824分享
- 2024-07-28·技术支持
AI 超级智能体 - 2 - AI 大模型接入 笔记
AI 超级智能体 - 2 笔记,直接弄成卡片更直观 <img src="https://pic.code-nav.cn/post_picture/1620630456775032833/rIMA51lprpBzAAWg.webp" alt="image.png" width="931px" />
智能协同云图库项目 第 5 期 用户传图 前端笔记
# 审核信息相关 1、定义审核常量 位置`src/constants/picture.ts` ```ts export const PIC_REVIEW_STATUS_ENUM = { REVIEWING: 0, PASS: 1, REJECT: 2, } export const PIC_REVIEW_STATUS_MAP = { 0: '待审核', 1: '通过', 2: '拒绝', } export const PIC_REVIEW_STATUS_OPTIONS = Object.keys(PIC_REVIEW_STATUS_MAP).map((key) => { return { label: PIC_REVIEW_STATUS_MAP[key], value: key, } }) ``` 2、在 `PictureManagePage` 增加审核字段 放到 `用户 id` 和 `创建时间` 之间 ``` const columns = [ // ... { title: '审核信息', dataIndex: 'reviewMessage', }, // ... ] ``` 3、增加之自定义要展示的内容 `src/pages/admin/PictureManagePage.vue` ```vue <!-- 审核信息 --> <template v-if="column.dataIndex === 'reviewMessage'"> <div>审核状态:{{ PIC_REVIEW_STATUS_MAP[record.reviewStatus] }}</div> <div>审核信息:{{ record.reviewMessage }}</div> <div>审核人:{{ record.reviewerId }}</div> </template> ``` 4、新增过审和拒绝操作 1)增加通过和拒绝按钮 ```vue <template v-else-if="column.key === 'action'"> <a-space wrap> <a-button v-if="record.reviewStatus !== PIC_REVIEW_STATUS_ENUM.PASS" type="link" @click="handleReview(record, PIC_REVIEW_STATUS_ENUM.PASS)" > 通过 </a-button> <a-button v-if="record.reviewStatus !== PIC_REVIEW_STATUS_ENUM.REJECT" type="link" danger @click="handleReview(record, PIC_REVIEW_STATUS_ENUM.REJECT)" > 拒绝 </a-button> <a-button type="link" :href="`/add_picture?id=${record.id}`" target="_blank" >编辑 </a-button> <a-button type="link" danger @click="doDelete(record.id)">删除</a-button> </a-space> </template> ``` 2)编写 handleReview 方法 ```ts const handleReview = async (record: API.Picture, reviewStatus: number) => { const reviewMessage = reviewStatus === PIC_REVIEW_STATUS_ENUM.PASS ? '管理员操作通过' : '管理员操作拒绝' const res = await doPictureReviewUsingPost({ id: record.id, reviewStatus, reviewMessage, }) if (res.data.code === 0) { message.success('审核操作成功') // 重新获取列表 fetchData() } else { message.error('审核操作失败,' + res.data.message) } } ``` 5、按照需求审核状态筛选 ```vue <a-form-item label="审核状态" name="reviewStatus"> <a-select v-model:value="searchParams.reviewStatus" :options="PIC_REVIEW_STATUS_OPTIONS" placeholder="请输入审核状态" style="min-width: 180px" allow-clear /> </a-form-item> ``` --- 修改完成后最终的代码 `PictureManagePage.vue` ```vue <template> <div id="userManagePage"> <a-flex justify="space-between"> <h2>图片管理</h2> <a-button type="primary" href="/add_picture" target="_blank">+ 创建图片</a-button> </a-flex> <a-form layout="inline" :model="searchParams" @finish="doSearch"> <a-form-item label="关键词" name="searchText"> <a-input v-model:value="searchParams.searchText" placeholder="从名称和简介搜索" allow-clear /> </a-form-item> <a-form-item label="类型" name="category"> <a-input v-model:value="searchParams.category" placeholder="请输入类型" allow-clear /> </a-form-item> <a-form-item label="标签" name="tags"> <a-select v-model:value="searchParams.tags" mode="tags" placeholder="请输入标签" style="min-width: 180px" allow-clear /> </a-form-item> <a-form-item label="审核状态" name="reviewStatus"> <a-select v-model:value="searchParams.reviewStatus" :options="PIC_REVIEW_STATUS_OPTIONS" placeholder="请输入审核状态" style="min-width: 180px" allow-clear /> </a-form-item> <a-form-item> <a-button type="primary" html-type="submit">搜索</a-button> </a-form-item> </a-form> <div style="margin-bottom: 16px" /> <!-- 表格 --> <a-table :columns="columns" :data-source="dataList" :pagination="pagination" @change="doTableChange" > <template #bodyCell="{ column, record }"> <template v-if="column.dataIndex === 'url'"> <a-image :src="record.url" :width="120" /> </template> <!-- 标签 --> <template v-if="column.dataIndex === 'tags'"> <a-space wrap> <a-tag v-for="tag in JSON.parse(record.tags || '[]')" :key="tag">{{ tag }}</a-tag> </a-space> </template> <!-- 图片信息 --> <template v-if="column.dataIndex === 'picInfo'"> <div>格式:{{ record.picFormat }}</div> <div>宽度:{{ record.picWidth }}</div> <div>高度:{{ record.picHeight }}</div> <div>宽高比:{{ record.picScale }}</div> <div>大小:{{ (record.picSize / 1024).toFixed(2) }}KB</div> </template> <!-- 审核信息 --> <template v-if="column.dataIndex === 'reviewMessage'"> <div>审核状态:{{ PIC_REVIEW_STATUS_MAP[record.reviewStatus] }}</div> <div>审核信息:{{ record.reviewMessage }}</div> <div>审核人:{{ record.reviewerId }}</div> </template> <template v-else-if="column.dataIndex === 'createTime'"> {{ dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss') }} </template> <template v-else-if="column.dataIndex === 'editTime'"> {{ dayjs(record.editTime).format('YYYY-MM-DD HH:mm:ss') }} </template> <template v-else-if="column.key === 'action'"> <a-space wrap> <a-button v-if="record.reviewStatus !== PIC_REVIEW_STATUS_ENUM.PASS" type="link" @click="handleReview(record, PIC_REVIEW_STATUS_ENUM.PASS)" > 通过 </a-button> <a-button v-if="record.reviewStatus !== PIC_REVIEW_STATUS_ENUM.REJECT" type="link" danger @click="handleReview(record, PIC_REVIEW_STATUS_ENUM.REJECT)" > 拒绝 </a-button> <a-button type="link" :href="`/add_picture?id=${record.id}`" target="_blank" >编辑 </a-button> <a-button type="link" danger @click="doDelete(record.id)">删除</a-button> </a-space> </template> </template> </a-table> </div> </template> <script lang="ts" setup> import { computed, onMounted, reactive, ref } from 'vue' import { message } from 'ant-design-vue' import dayjs from 'dayjs' import {deletePictureUsingPost, doPictureReviewUsingPost, listPictureByPageUsingPost} from '@/api/pictureController.ts' import {PIC_REVIEW_STATUS_ENUM, PIC_REVIEW_STATUS_MAP, PIC_REVIEW_STATUS_OPTIONS} from "@/constants/picture.ts"; const columns = [ { title: 'id', dataIndex: 'id', width: 80, }, { title: '图片', dataIndex: 'url', }, { title: '名称', dataIndex: 'name', }, { title: '简介', dataIndex: 'introduction', ellipsis: true, }, { title: '类型', dataIndex: 'category', }, { title: '标签', dataIndex: 'tags', }, { title: '图片信息', dataIndex: 'picInfo', }, { title: '用户 id', dataIndex: 'userId', width: 80, }, { title: '审核信息', dataIndex: 'reviewMessage', }, { title: '创建时间', dataIndex: 'createTime', }, { title: '编辑时间', dataIndex: 'editTime', }, { title: '操作', key: 'action', }, ] // 数据 const dataList = ref([]) const total = ref(0) // 搜索条件 const searchParams = reactive<API.PictureQueryRequest>({ current: 1, pageSize: 10, sortField: 'createTime', sortOrder: 'descend', }) // 分页参数 const pagination = computed(() => { return { current: searchParams.current ?? 1, pageSize: searchParams.pageSize ?? 10, total: total.value, showSizeChanger: true, showTotal: (total) => `共 ${total} 条`, } }) // 获取数据 const fetchData = async () => { const res = await listPictureByPageUsingPost({ ...searchParams, }) if (res.data.data) { dataList.value = res.data.data.records ?? [] total.value = res.data.data.total ?? 0 } else { message.error('获取数据失败,' + res.data.message) } } // 获取数据 const doSearch = () => { // 重置搜索条件 searchParams.current = 1 fetchData() } // 表格变化处理 const doTableChange = (page: any) => { searchParams.current = page.current searchParams.pageSize = page.pageSize fetchData() } const doDelete = async (id: number) => { if (id == null) { message.error('id 为空') return } const deleteResponse = await deletePictureUsingPost({ id }) if (deleteResponse.data.data && deleteResponse.data.code === 0) { await fetchData() message.success('删除成功') } else { message.error('删除失败') } } const handleReview = async (record: API.Picture, reviewStatus: number) => { const reviewMessage = reviewStatus === PIC_REVIEW_STATUS_ENUM.PASS ? '管理员操作通过' : '管理员操作拒绝' const res = await doPictureReviewUsingPost({ id: record.id, reviewStatus, reviewMessage, }) if (res.data.code === 0) { message.success('审核操作成功') // 重新获取列表 fetchData() } else { message.error('审核操作失败,' + res.data.message) } } // 页面加载时请求一次 onMounted(() => { fetchData() }) </script> <style scoped> .picture-upload :deep(.ant-upload) { width: 100% !important; height: 100% !important; min-height: 152px; min-width: 152px; } .picture-upload img { max-width: 100%; max-height: 480px; } .ant-upload-select-picture-card i { font-size: 32px; color: #999; } .ant-upload-select-picture-card .ant-upload-text { margin-top: 8px; color: #666; } </style> ``` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/iVQt69fBJyreVJGM.webp" alt="image-20241222235709052" width="100%" /> # URL 上传组件 来源:https://antdv.com/components/input-cn#components-input-demo-group 1、在 `src/components/UrlPictureUpload.vue` 创建一下组件 ```vue <template> <div class="url-picture-upload"> <a-input-group compact> <a-input v-model:value="fileUrl" style="width: calc(100% - 120px)" placeholder="请输入图片地址" /> <a-button type="primary" style="width: 120px" :loading="loading" @click="handleUpload"> 提交 </a-button> </a-input-group> <div class="img-wrapper"> <img v-if="picture?.url" :src="picture?.url" alt="avatar" /> </div> </div> </template> <script lang="ts" setup> import { ref } from 'vue' import { message } from 'ant-design-vue' import { uploadPictureByUrlUsingPost } from '@/api/pictureController.ts' interface Props { picture?: API.PictureVO spaceId?: number onSuccess?: (newPicture: API.PictureVO) => void } const props = defineProps<Props>() const fileUrl = ref<string>() const loading = ref<boolean>(false) /** * 上传图片 */ const handleUpload = async () => { loading.value = true try { const params: API.PictureUploadRequest = { fileUrl: fileUrl.value } if (props.picture) { params.id = props.picture.id } const res = await uploadPictureByUrlUsingPost(params) if (res.data.code === 0 && res.data.data) { message.success('图片上传成功') // 将上传成功的图片信息传递给父组件 props.onSuccess?.(res.data.data) } else { message.error('图片上传失败,' + res.data.message) } } catch (error) { console.error('图片上传失败', error) message.error('图片上传失败,' + error.message) } loading.value = false } </script> <style scoped> .url-picture-upload { margin-bottom: 16px; } .url-picture-upload img { max-width: 100%; max-height: 480px; } .url-picture-upload .img-wrapper { text-align: center; margin-top: 16px; } </style> ``` 2、修改 `src/pages/AddPicturePage.vue` ```vue <!-- 选择上传方式 --> <a-tabs v-model:activeKey="uploadType" >> <a-tab-pane key="file" tab="文件上传"> <PictureUpload :picture="picture" :onSuccess="onSuccess" /> </a-tab-pane> <a-tab-pane key="url" tab="URL 上传" force-render> <UrlPictureUpload :picture="picture" :onSuccess="onSuccess" /> </a-tab-pane> </a-tabs> ``` 在 `script` 上添加一下代码 ````ts const uploadType = ref<'file' | 'url'>('file') ```` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/p8yF6qJ6f53JpA12.webp" alt="image-20241223003804388" width="100%" /> --- `src/pages/AddPicturePage.vue` 具体位置 ```vue <template> <div id="addPicturePage"> <h2 style="margin-bottom: 16px"> {{ route.query?.id ? '修改图片' : '创建图片' }} </h2> <!-- 选择上传方式 --> <a-tabs v-model:activeKey="uploadType" >> <a-tab-pane key="file" tab="文件上传"> <PictureUpload :picture="picture" :onSuccess="onSuccess" /> </a-tab-pane> <a-tab-pane key="url" tab="URL 上传" force-render> <UrlPictureUpload :picture="picture" :onSuccess="onSuccess" /> </a-tab-pane> </a-tabs> <!-- 图片信息表单 --> <a-form v-if="picture" name="pictureForm" layout="vertical" :model="pictureForm" @finish="handleSubmit" > <a-form-item name="name" label="名称"> <a-input v-model:value="pictureForm.name" placeholder="请输入名称" allow-clear /> </a-form-item> <a-form-item name="introduction" label="简介"> <a-textarea v-model:value="pictureForm.introduction" placeholder="请输入简介" :auto-size="{ minRows: 2, maxRows: 5 }" allow-clear /> </a-form-item> <a-form-item name="category" label="分类"> <a-auto-complete v-model:value="pictureForm.category" placeholder="请输入分类" :options="categoryOptions" allow-clear /> </a-form-item> <a-form-item name="tags" label="标签"> <a-select v-model:value="pictureForm.tags" mode="tags" placeholder="请输入标签" :options="tagOptions" allow-clear /> </a-form-item> <a-form-item> <a-button type="primary" html-type="submit" style="width: 100%">创建</a-button> </a-form-item> </a-form> </div> </template> <script setup lang="ts"> import PictureUpload from '@/components/PictureUpload.vue' import { onMounted, reactive, ref } from 'vue' import { message } from 'ant-design-vue' import { editPictureUsingPost, getPictureVoByIdUsingGet, listPictureTagCategoryUsingGet, } from '@/api/pictureController.ts' import { useRoute, useRouter } from 'vue-router' import UrlPictureUpload from '@/components/UrlPictureUpload.vue' const picture = ref<API.PictureVO>() const pictureForm = reactive<API.PictureEditRequest>({}) const uploadType = ref<'file' | 'url'>('file') /** * 图片上传成功 * @param newPicture */ const onSuccess = (newPicture: API.PictureVO) => { picture.value = newPicture pictureForm.name = newPicture.name } const router = useRouter() /** * 提交表单 * @param values */ const handleSubmit = async (values: any) => { console.log(values) const pictureId = picture.value.id if (!pictureId) { return } const res = await editPictureUsingPost({ id: pictureId, ...values, }) // 操作成功 if (res.data.code === 0 && res.data.data) { message.success('创建成功') // 跳转到图片详情页 router.push({ path: `/picture/${pictureId}`, }) } else { message.error('创建失败,' + res.data.message) } } const categoryOptions = ref<string[]>([]) const tagOptions = ref<string[]>([]) /** * 获取标签和分类选项 * @param values */ const getTagCategoryOptions = async () => { const res = await listPictureTagCategoryUsingGet() if (res.data.code === 0 && res.data.data) { tagOptions.value = (res.data.data.tagList ?? []).map((data: string) => { return { value: data, label: data, } }) categoryOptions.value = (res.data.data.categoryList ?? []).map((data: string) => { return { value: data, label: data, } }) } else { message.error('获取标签分类列表失败,' + res.data.message) } } const route = useRoute() // 获取老数据 const getOldPicture = async () => { // 获取到 id const id = route.query?.id if (id) { const res = await getPictureVoByIdUsingGet({ id, }) if (res.data.code === 0 && res.data.data) { const data = res.data.data picture.value = data pictureForm.name = data.name pictureForm.introduction = data.introduction pictureForm.category = data.category pictureForm.tags = data.tags } } } onMounted(() => { getOldPicture() getTagCategoryOptions() }) </script> <style scoped> #addPicturePage { max-width: 720px; margin: 0 auto; } </style> ``` # 批量创建页面 新增管理员 `批量创建图片` 1、新增页面 ```VUE <template> <div id="addPictureBatchPage"> <h2 style="margin-bottom: 16px">批量创建图片</h2> <a-form layout="vertical" :model="formData" @finish="handleSubmit"> <a-form-item label="关键词" name="searchText"> <a-input v-model:value="formData.searchText" placeholder="请输入关键词" /> </a-form-item> <a-form-item label="抓取数量" name="count"> <a-input-number v-model:value="formData.count" placeholder="请输入数量" style="min-width: 180px" :min="1" :max="30" allow-clear /> </a-form-item> <a-form-item label="名称前缀" name="namePrefix"> <a-input v-model:value="formData.namePrefix" placeholder="请输入名称前缀,会自动补充序号" /> </a-form-item> <a-form-item> <a-button type="primary" html-type="submit" style="width: 100%" :loading="loading"> 执行任务 </a-button> </a-form-item> </a-form> </div> </template> <script setup lang="ts"> import { reactive, ref } from 'vue' import { message } from 'ant-design-vue' import { uploadPictureByBatchUsingPost } from '@/api/pictureController.ts' import { useRouter } from 'vue-router' const formData = reactive<API.PictureUploadByBatchRequest>({ count: 10, }) // 提交任务状态 const loading = ref(false) const router = useRouter() /** * 提交表单 * @param values */ const handleSubmit = async (values: any) => { loading.value = true const res = await uploadPictureByBatchUsingPost({ ...formData, }) // 操作成功 if (res.data.code === 0 && res.data.data) { message.success(`创建成功,共 ${res.data.data} 条`) // 跳转到主页 router.push({ path: `/`, }) } else { message.error('创建失败,' + res.data.message) } loading.value = false } </script> <style scoped> #addPictureBatchPage { max-width: 720px; margin: 0 auto; } </style> ``` 2、增加 router.ts 文件 ```ts { path: '/add_picture/batch', name: '批量创建图片', component: AddPictureBatchPage, }, ``` 3、在管理员页面 `src/pages/admin/PictureManagePage.vue` ,添加 ```vue <a-space> <a-button type="primary" href="/add_picture" target="_blank">+ 创建图片</a-button> <a-button type="primary" href="/add_picture/batch" target="_blank" ghost>+ 批量创建图片</a-button> </a-space> ``` <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/6XiPvgiDFDBMzAua.webp" alt="image-20241223161857621" width="100%" /> 4、测试 <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/EdN2IOu09UHV0mZa.webp" alt="image-20241223162538302" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1608460212774109186/TBJEU3KabCCL3N6z.webp" alt="image-20241223162546602" width="100%" />
面试刷题平台 前端开发 md扩展
<h1 id="us1KG">修复代码高亮丢失</h1> 上期扩展,我将题目详情增加了 select 选择框,但是有一个问题,我们每次切换主题时候,代码的高亮会丢失 效果: <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/kECgEAsXhsldw8Wt.png" alt="" width="100%" /> 代码是引入高亮的: ```tsx import highlight from "@bytemd/plugin-highlight"; const plugins = [gfm(), highlight()]; ``` 查看页面请求回来的数据: <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/QFgGFJeuPvpqSrJf.webp" alt="" width="100%" /> 从这里可以看出,在渲染的时候就没有将代码渲染成高亮的格式,我们用的是服务端渲染,代码高亮的插件是支持 SSR 的所以我们重新安装一个代码高亮的插件: ```tsx npm i @bytemd/plugin-highlight-ssr ``` 然后将 MdViewer 的代码高亮度插件替换一下: ```tsx import highlight from "@bytemd/plugin-highlight-ssr"; ``` 然后再次刷新页面 查看回来的代码: <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/vrd5rDZOf4UEaXcE.webp" alt="" width="100%" /> 这次渲染的代码就有高亮的类名了! <h1 id="X7PgC">通用图片上传功能</h1> 官方文档中对于 Editor 是有图片上传功能的 地址:[https://bytemd.netlify.app/#options](https://bytemd.netlify.app/#options) <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/b8NmfZRTqT86WnTY.webp" alt="" width="100%" /> 直接在 MdEditor 中增加属性: ```tsx uploadImages={async (files: any) => { let imgUrl = ""; const res = await uploadFile( { biz: "user_upload", }, {}, files[0], ); // @ts-ignore if (res.data.code === 0) { // @ts-ignore imgUrl = res.data.data; // 这里是上传成功后,服务端返回的图片地址 } else { console.log("图片上传失败"); } console.log(res); return [ { title: files.map((i: any) => i.name), url: imgUrl, }, ]; }} ``` 这里biz 对应后端的上传类型,自行设置下就好 <h2 id="jWKBu">图片预览</h2> 官方文档中的插件中有图片预览功能,这里我也集成下: <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/ORMRI6leeulLKsbY.webp" alt="" width="100%" /> 1. 安装依赖 ```tsx npm i @bytemd/plugin-medium-zoom ``` 2. 引入依赖 ```tsx import mediumZoom from '@bytemd/plugin-medium-zoom'; const plugins = [ gfm(), highlight(), theme({ themeList, }), mediumZoom(), ]; ``` 这里有个小问题,就是我们全屏编辑器后,点击预览图片,图片没有在最上层,我们关闭编辑器后,预览的图片才显示,这里修改下样式文件: ```tsx .medium-zoom-image { z-index: 9999999 !important; } ``` <h1 id="zfaCP">代码块复制功能</h1> 浏览面试鸭官网,发现展示代码块时,有一个收缩功能和一个复制功能,这里我也想实现下,于是我发现官网是有一个自定义插件的!(之前没找到.....) 效果: <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/a0A3LoBkoZKZLT2u.webp" alt="" width="100%" /> 这道题不是仅 VIP 可见,我就粘到这里了... 应该没啥事吧, 在我查询实现的过程中,发现面试鸭和掘金的这个复制功能看着还挺像的 <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/WfapuYzwkCv6vkjm.webp" alt="" width="100%" /> 于是,我跟着官网还有搜索到的文档实现了一个 `code()` 插件 官网:[https://bytemd.netlify.app/#authoring-a-plugin](https://bytemd.netlify.app/#authoring-a-plugin) <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/Np8jDvVdi1qyOTkM.webp" alt="" width="100%" /> <h2 id="aYfyx">实现</h2> 这里实现需要一些学过一些 js 基础 在 src 目录下新建`bytemdPlugin`目录,然后新建一个`ByteCode`目录,再新建 index.tsx 和 index.css <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/NyyWbX7Duazksihw.png" alt="" width="240px" /> 调用复制代码的方法: ```tsx // 拷贝代码 const copyToClipboard = (text: string) => navigator.clipboard.writeText(text); ``` 增加头部布局,左侧是点击缩小,右侧展示语言和复制代码的功能 ```tsx // innerHTML 布局 const codeText: any = ( innerHTML: HTMLElement, key: number, backgroundColor: string, lan: string, ) => { return ( <div className={"code-block-extension-header code-header-" + key} style={{ backgroundColor }} > <div className="code-block-extension-headerLeft"> <div className={"code-block-extension-foldBtn foldBtn-" + key}> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M16.924 9.617A1 1 0 0 0 16 9H8a1 1 0 0 0-.707 1.707l4 4a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 .217-1.09z" data-name="Down" ></path> </svg> </div> </div> <div className="code-block-extension-headerRight"> <span className="code-block-extension-lang">{lan}</span> <span className={ "code-block-extension-lang code-block-extension-copy code-block-extension-copy-" + key } > 复制代码 </span> </div> </div> ); }; ``` 具体渲染逻辑: ```tsx // 渲染页面元素 export const renderEle = function (code: HTMLElement, key: number) { let className: string = code.className; if (!className ?? "") { return; } // 使用正则表达式匹配以 "language-" 开头的类名 const match = className.match(/language-(.+)/); // 如果匹配成功,提取语言部分并转换为大写 let lan = match ? match[1].toUpperCase() : ""; const parentNode: any = code.parentNode; // 获取计算后的样式 const style = window.getComputedStyle(code); // 获取背景色 const backgroundColor = style.backgroundColor; parentNode.removeChild(code); parentNode.innerHTML = ReactDOMServer.renderToString(codeText(code, key, backgroundColor, lan)) + '<code id="code-' + key + '" class="' + code.className + '">' + code.innerHTML + "</code>"; let btn: any = document.getElementsByClassName( "code-block-extension-copy-" + key, )[0]; let timeoutID: any = null; // 用于存储 setTimeout 的 ID const restoreText = () => { btn.innerHTML = "复制代码"; btn.disable = false; }; const timeout = () => { timeoutID = setTimeout(restoreText, 3000); // 设置定时器,三秒后执行 restoreText 函数 }; btn.addEventListener("click", () => { if (btn.disable) { return; } clearTimeout(timeoutID); // 清除之前的定时器 copyToClipboard(code.innerText); // 执行复制操作 btn.innerHTML = "Copied"; // 立即更改按钮文本 btn.disable = true; message.success({ content: "复制成功", }); timeout(); // 设置新的定时器,三秒后恢复按钮文本 }); // 旋转隐藏code let foldBtn: any = document.getElementsByClassName("foldBtn-" + key)[0]; // 最上层的父类的盒子阴影也去掉 let codeHeader: any = document.getElementsByClassName( "code-header-" + key, )[0]; foldBtn.addEventListener("click", () => { let codeEle = document.getElementById("code-" + key); codeEle?.classList.toggle("hidden"); foldBtn.classList.toggle("rotate90"); codeHeader.classList.toggle("boxShadowNone"); }); }; ``` 完整代码: ```tsx import type { BytemdPlugin } from "bytemd"; import hljs from "highlight.js"; import ReactDOMServer from "react-dom/server"; import "./index.css"; import { message } from "antd"; // 拷贝代码 const copyToClipboard = (text: string) => navigator.clipboard.writeText(text); // 渲染页面元素 export const renderEle = function (code: HTMLElement, key: number) { let className: string = code.className; if (!className ?? "") { return; } // 使用正则表达式匹配以 "language-" 开头的类名 const match = className.match(/language-(.+)/); // 如果匹配成功,提取语言部分并转换为大写 let lan = match ? match[1].toUpperCase() : ""; const parentNode: any = code.parentNode; // 获取计算后的样式 const style = window.getComputedStyle(code); // 获取背景色 const backgroundColor = style.backgroundColor; parentNode.removeChild(code); parentNode.innerHTML = ReactDOMServer.renderToString(codeText(code, key, backgroundColor, lan)) + '<code id="code-' + key + '" class="' + code.className + '">' + code.innerHTML + "</code>"; let btn: any = document.getElementsByClassName( "code-block-extension-copy-" + key, )[0]; let timeoutID: any = null; // 用于存储 setTimeout 的 ID const restoreText = () => { btn.innerHTML = "复制代码"; btn.disable = false; }; const timeout = () => { timeoutID = setTimeout(restoreText, 3000); // 设置定时器,三秒后执行 restoreText 函数 }; btn.addEventListener("click", () => { if (btn.disable) { return; } clearTimeout(timeoutID); // 清除之前的定时器 copyToClipboard(code.innerText); // 执行复制操作 btn.innerHTML = "Copied"; // 立即更改按钮文本 btn.disable = true; message.success({ content: "复制成功", }); timeout(); // 设置新的定时器,三秒后恢复按钮文本 }); // 旋转隐藏code let foldBtn: any = document.getElementsByClassName("foldBtn-" + key)[0]; // 最上层的父类的盒子阴影也去掉 let codeHeader: any = document.getElementsByClassName( "code-header-" + key, )[0]; foldBtn.addEventListener("click", () => { let codeEle = document.getElementById("code-" + key); codeEle?.classList.toggle("hidden"); foldBtn.classList.toggle("rotate90"); codeHeader.classList.toggle("boxShadowNone"); }); }; // innerHTML 布局 const codeText: any = ( innerHTML: HTMLElement, key: number, backgroundColor: string, lan: string, ) => { return ( <div className={"code-block-extension-header code-header-" + key} style={{ backgroundColor }} > <div className="code-block-extension-headerLeft"> <div className={"code-block-extension-foldBtn foldBtn-" + key}> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M16.924 9.617A1 1 0 0 0 16 9H8a1 1 0 0 0-.707 1.707l4 4a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0 .217-1.09z" data-name="Down" ></path> </svg> </div> </div> <div className="code-block-extension-headerRight"> <span className="code-block-extension-lang">{lan}</span> <span className={ "code-block-extension-lang code-block-extension-copy code-block-extension-copy-" + key } > 复制代码 </span> </div> </div> ); }; export default function code(): BytemdPlugin { return { viewerEffect({ markdownBody }): void | (() => void) { const codes = markdownBody.querySelectorAll("code"); codes.forEach((code, key) => { renderEle(code, key); }); } }; } ``` 这里仿照掘金的样式,样式直接 copy 掘金的,使用控制台直接拷贝 ```tsx .code-block-extension-header { display: flex; user-select: none; height: 28px; align-items: center; justify-content: space-between; box-shadow: 0 5px 4px -6px #888888; margin-bottom: 1px; } .code-block-extension-headerLeft { margin-left: 10px; } .code-block-extension-headerLeft, .code-block-extension-headerRight { display: flex; align-items: center; } .code-block-extension-headerLeft > .code-block-extension-foldBtn { display: flex; align-items: center; justify-content: center; width: 18px; height: 18px; filter: invert(0.8); } .code-block-extension-headerRight { margin-right: 10px; } .code-block-extension-lang { margin-left: 20px; font-size: 12px; filter: invert(0.5); opacity: 0.6; } .code-block-extension-copy{ cursor: pointer; } .code-block-extension-foldBtn:hover{ color: #ffffff; cursor: pointer; } .rotate90{ transform: rotate(-90deg); } .hidden { display: none !important; } .boxShadowNone{ box-shadow: none !important; margin-bottom: 0 !important; } ``` 然后我们在 MdViewer 中引入 ```tsx import code from "../../bytemdPlugin/ByteCode"; const plugins = [gfm(), highlight(), code(), mediumZoom()]; ``` 样式效果: <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/SbFiobcKhqI3CGp6.webp" alt="" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/gmC5we65q0IOcD1z.webp" alt="" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/qGCF2FL5iD9qrz1C.webp" alt="" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/1ihDPIdMFO0aB5hQ.webp" alt="" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1707418316274003969/EcIldQf9unZ0j2AF.webp" alt="" width="100%" /> <h2 id="DmgST">bug 修复</h2> 如果有实现上期扩展,根据选择展示不同的主题,那么在选择时候这个复制的功能就消失了,这里给出修复的代码:(不是专业前段,如有好的解决办法,希望大家指点) ```tsx const MdViewer = (props: Props) => { const { value = "", theme = "channing-cyan" } = props; const viewerRef = useRef(); useEffect(() => { setTheme(theme); // @ts-ignore plugins[2].viewerEffect({ markdownBody: viewerRef.current }); }, [theme]); return ( // @ts-ignore <div className="md-viewer" ref={viewerRef}> <Viewer value={value} plugins={plugins} /> </div> ); }; ```
项目学习[更新完结] - API开发平台个人笔记总结梳理 + 简单扩展思路
# API 开放平台项目 [学习笔记+开发+简单优化扩展+部署] - 总结梳理 ## 背景介绍和需求分析 简介: 一个 API 接口提供开发者调用的平台,基于 SpringBoot 后端 + React 前端项目 GitHub 仓库: https://github.com/Jools-hzx/JoolsAPIs-Open-Platform ### **实现功能** **管理员权限:** 1. 接入并发布接口 2. 可视化各接口调用情况 3. 发布配置限流、熔断规则 (扩展点) **用户权限:** 1. 开通接口调用权限 2. 浏览接口及在线调试 3. 通过客户端 SDK 轻松调用接口 ### **涉及技术栈:** - 后端:Java, Spring Boot, Spring Cloud Gateway, Dubbo, MyBatis, Nacos, Sentinel - 前端:Ant Design Pro, ECharts - 工具:Maven, Git, Nacos, API 签名认证, 限流熔断 ### 项目架构图[已更新,整合 Seata] <img src="https://pic.code-nav.cn/post_picture/1819386569259450369/Y2CeoYY9FaCx8XRz.webp" alt="项目架构图.jpg" width="100%" /> ### 项目划分 | **子系统划分** | **描述** | |------------------------|--------------------------------------------------------------------------| | 模拟接口系统 | 提供各种模拟接口供开发者使用和测试 | | 后台管理系统 | 管理员可以发布接口、设置接口的调用数量、设定是否下线接口,以及查看用户使用情况,如使用次数、错误调用等 | | 用户前台系统 | 提供一个访问界面,供开发者浏览所有的接口 | | API 网关系统 | 负责接口的流量控制 | | 第三方调用 SDK 系统 | 提供一个简化的工具包,供开发者调用 API | ### 实际搭建 | **项目搭建模块** | **描述** | |------------------------|--------------------------------------------------------------------------| | 后台项目 | 后台管理系统的实际项目搭建 | | 网关项目 | API 网关系统的实际项目搭建 | | SDK 项目 | 第三方调用 SDK 系统的实际项目搭建 | | 前端项目 | 用户前台系统的实际项目搭建 | | 模拟接口项目 | 模拟接口系统的实际项目搭建 | | 抽取公共模块项目 | 提供各个系统共用的通用模块搭建 | ## 各阶段实现和笔记 ### 实现阶段一: 项目前后端框架搭建 阶段一笔记: https://reurl.cc/qvp1DE 主要内容: - 搭建前端项目和后端项目初始框架,打通前后端数据通信 - 前端基于 Ant Design Pro + Umi 的鱼皮前端通用模板 - 后端基于鱼皮后端通过模板 - 设计 接口信息表 (借助 sqlfather 工具快速生成 SQL 语句建表) - 借助 OpenApi接口文档规范自动生成前后端通信接口方法 ### 实现阶段二: 前端扩展、签名认证、SDK开发 阶段二笔记:https://reurl.cc/Eg3myn 主要内容: - 完善前端页面和发送请求到后端的接口 - 开发模拟接口平台项目,开发简易的客户端模拟调用接口 - 开发 API 签名认证,保证安全性 - 客户端 SDK 的开发 - SPI 机制知识点学习 - 完善前端接口展示、接口在线调用页面 ### 实现阶段三: 实现接口上线、下线、统计调用次数功能 笔记: https://reurl.cc/7dE3od 主要内容: - 实现功能:完成接口发布/上线、下线功能 - 完善前端可以浏览接口、查看接口文档、申请调用接口 - 完善后台可以统计用户调用接口的次数 - 学习网关的概念、作用和引入的网关步骤 ### 实现阶段四: 完善用户接口权限关系、调用次数;学习网关 笔记: https://reurl.cc/zDQWbQ 主要内容: - 完善用户调用接口关系维护。 - 调用成功之后调用次数 + 1,完成请求接口编写 - 不足之处分析: - 调用大量接口次数, 需要涉及到事务和锁的知识 - 需要使用分布式锁来保证数据的一致性 - 学习 SpringCloud Gateway ### 实现阶段五: 开发网关项目 笔记: https://reurl.cc/93W5OY 主要内容: - 使用 SpringCloud Gateway 组件开发网关项目 - 使用全局过滤器组件完成鉴权、转发、日志输入等业务 - 前端用户可走通调用接口整个流程,返回响应结果 ### 实现阶段六: RPC 和 Dubbo 框架学习 笔记: https://reurl.cc/dypxWy 主要内容: - RPC 介绍和 Dubbo 框架学习 - 基于 Dubbo 官方手册学习 - 阅读官方示例代码 - 基于官方示例代码搭建 Demo 测试 - 结合 Demo 示例,整合 Dubbo 到项目内实现方法远程调用 ### 实现阶段七: 抽取公共模块、改造项目整合Dubbo、开发可视化功能 笔记: https://reurl.cc/OrZ447 主要内容: - 抽取公共接口、model为公共模块发布到本地仓库 - 后台项目导入公共模块改造 - 实现公共接口, 实现方法用@DubboService注释 - 网关项目导入公共模块改造,远程调用Dubbo服务提供实现方法 - 完善调用逻辑,测试整个流程 - 开发前端可视化功能 - 基于 EChart ### 实现阶段八(简单扩展): 扩充网关功能和网关优化(增加限流、降级机制) - 详见笔记: https://reurl.cc/8X8p6d - 标题 - `优化01 - 基于 Sentinel 组件配合配合网关完成限流、降级控制、流控规则持久化` 主要内容: - `完善` - 网关校验该用户是否剩余调用次数是否合法 - `扩展` - 使用 Sentinel + Nacos 完成限流 + 降级 + 流控规则持久化 - `扩展` - 基于Sentinel 适配 Gateway 的网关限流规则完成限流规则设置 - `扩展` -采用 Nacos 作为 Sentinel 规则配置数据源整合,防止 Sentinel 应用重启规则丢失 - `扩展` -基于 Nacos 统一管理流控规则,发布、修改和维护限流规则和降级规则 ### 实现阶段九(扩展): 引入分布式事务 Seata, 控制调用接口 - 详见笔记: https://reurl.cc/8X8p6d - 标题 - `优化02 —— 引入分布式事务 Seata, 控制调用接口` 主要内容: - `扩展` - 需求分析,分布式事务,学习 Seata 组件 - `学习` - Seata入门 [ 基于官方文档 ],相关术语,Seata实现分布式事务的流程机制与分析 - `学习` - 事务模式 AT - `学习` - 基于官方给的 sample 学习整合 Seata + SpringBoot + Dubbo - `扩展` - 将 Seata 整合进后端项目,Nacos作为配置和服务注册中心,基于命名空间统一管理 - `测试` - 测试分布式事务 ### 实现部署 - 基于 Docker + 宝塔面板 + 腾讯云服务器 + Xshell + Xftp - 详见笔记: https://reurl.cc/rvbRKZ - 标题 - ` API接口平台- 09 部署` #### 参考鱼友部署笔记 1. https://www.codefather.cn/post/1828449609845428226 2. https://www.codefather.cn/post/1829798511080284161 #### 部署遇到的问题 + 解决 - 单独整理了解决各个问题的笔记: https://reurl.cc/pvbMpa - 标题 - `部署问题-梳理` #### 遇到和解决的问题有: - **问题一:** `Nacos` 容器配置了数据库持久化之后,启动失败 - **问题二:** 解决 `Seata` 容器启动注册 `seata server` 服务到 `Nacos` 失败 - **问题三:** 解决报错 `ERROR: can not get cluster name in registry config 'service.vgroupMapping.[xxxx-GROUP]', please make sure registry config correct` - **问题四:** 注册服务到云服务器内的 Nacos, 本地项目 Nacos Client 报错 `Caused by: com.alibaba.nacos.api.exception.NacosException: Client not connected, current status:STARTING` [*!! 参考的鱼友笔记也遇到过 !!*] - **问题五:** 后端项目尝试注册到远端 `seata server` 服务报错 `Can not register RM, err: can not connect to services-server` - **问题六:** 没有从 `Nacos` 找到 `Dubbo` 服务提供方注册的服务 - **问题七:** 如何配置多环境打包 JAR `感受`: 不尝试不知道,一尝试吓一跳!!!配置过程真是处处易踩坑。一个问题折磨一整天也不是没可能....(lll¬ω¬)... ## 小结 该项目通过创新的技术栈选择与精心设计,提供了一个高效、安全且易于扩展的基于分布式微服务架构形式的 API 开放平台。它不仅能够为开发者提供简便的接口调用方式,还通过一系列安全和性能优化措施保障了系统的稳定性和可扩展性。
OJ项目部署笔记
## 写在前面 **我部署的是没有进行微服务改造之前的。** 通过XShell连接阿里云服务器进行,使用Docker部署。 ## 部署步骤 分三部分 1. 代码沙箱部署 2. 后端部署 3. 前端部署 ## 代码沙箱部署 1. 本地通过Maven 打包好jar包 <img src="https://pic.code-nav.cn/post_picture/1626606778294771714/TBJ0mIc0cNZEdc7k.webp" alt="image.png" width="100%" /> **这里有个很坑的点:因为我项目是通过阿里云的连接创建的,在 `pom.xml` 文件中,创建项目的时候自动给你加上如图这行代码** <img src="https://pic.code-nav.cn/post_picture/1626606778294771714/4FNNKiLDUUfIFMcK.webp" alt="image.png" width="100%" /> 这导致docker 一直部署失败,会报一个错误 `no main manifest attribute` [参考博客](https://blog.csdn.net/qq_33697094/article/details/110549536) 只需去掉这行代码或者改成 `false` 即可 2. 上传jar包和Dockerfile文件 Dockerfile 文件内容: ```dockerfile FROM openjdk:8-jdk-alpine # Copy local code to the container image. WORKDIR /app COPY myoj-code-sandbox-0.0.1-SNAPSHOT.jar /app/myoj-code-sandbox-0.0.1-SNAPSHOT.jar EXPOSE 8090 # Run the web service on container startup. CMD ["java", "-jar", "/app/myoj-code-sandbox-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"] ``` **自行更改文件名和端口号** 3. 使用如下命令构建镜像 ```sh docker build -t xxx:xxx . ``` 命令解释(来自AI): * `docker build`:这是 Docker 的命令,用于根据指定路径下的 `Dockerfile` 构建一个镜像。 * `-t xxx:xxx`:这是一个标志(`-t` 表示 "tag"),用于给构建的镜像打标签。`xxx:xxx` 中,前一个 `xxx` 通常代表镜像的名称,后一个 `xxx` 代表镜像的版本号。举个例子,`myapp:1.0` 中,`myapp` 是镜像的名称,`1.0` 是版本号。 * `.`:表示当前目录。`Docker` 会在这个目录下查找 `Dockerfile` 并根据它构建镜像。 4. 使用镜像构建一个容器 ```sh docker run -d -p 8090:8090 --name myapp myapp:1.0 ``` 具体解释如下: * `docker run`:这是 `Docker` 命令,用于运行一个容器。 * `-d`:以 "detached" 模式运行容器,即后台运行。容器启动后不会占据当前终端,允许你继续执行其他命令。 * `-p 8090:8090`:端口映射。将宿主机的 `8090` 端口映射到容器的 `8090` 端口。 * 左侧的 `8090`:宿主机上的端口号。 * 右侧的 `8090`:容器内的端口号。 这样当你访问宿主机的 `8090` 端口时,请求会转发到容器内部的 `8090` 端口。 * `--name myapp`:为这个容器指定一个名称 `myapp`。你可以通过这个名称来管理该容器。 * `myapp:1.0`:这是指定要运行的镜像名称和版本号。表示基于 `myapp:1.0` 镜像启动容器。 ## 部署MySQL MySQL我之前是部署过的,在部署这个项目的时候,我连接看了一下,发现里面的数据全不见了,并且里面还多了一个我看不懂的数据库。 <img src="https://pic.code-nav.cn/post_picture/1626606778294771714/YRN7dJZF3oA30ZBe.webp" alt="image.png" width="100%" /> 里面有两句话: > All your data is backed up. You must pay 0.0148 BTC to 1KRR5XEkckoo2d21d3FyZ9Spr6CnMEit7d In 48 hours, your data will be publicly disclosed and deleted. (more information: go to https://is.gd/fajofo) > > After payment send mail to us: dzen+25zwj@onionmail.org and we will provide a link for you to download your data. Your DBCODE is: 25ZWJ 意思就是你得打钱,我就把数据给你。 说明我被攻击了,因为我的密码是123456,随便一个人,只要知道你的服务器ip,就能拥有root账户,之后就是懂的都懂。 [参考博客](https://blog.csdn.net/weixin_46990523/article/details/137582729) 我把容器删了,重新部署一下,用了一个比较复杂的密码,目前数据还在(dog)。博客里头有其他解决办法,可以参考。 docker部署MySQL只需一行命令: ```bash docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 --name my_mysql mysql:8.0 ``` 本地找不到镜像会自动去 docker hub 拉取镜像,密码不要123456。更多信息和版本 [参考](https://hub.docker.com/_/mysql) **镜像下载不了的 [参考](https://blog.csdn.net/weixin_50160384/article/details/139861337) ** ## 后端部署 跟上面一样,打包 jar,dockerfile 只需改个端口号即可。上传到服务器。 ```bash docker build -t oj-backend . ``` ```bash docker run -d -p xxxx:xxxx --name oj-backend oj-backend ``` **注意这里改成远程的地址** <img src="https://pic.code-nav.cn/post_picture/1626606778294771714/RAGAdL0hdB68HVrZ.webp" alt="image.png" width="100%" /> ## 前端部署 1. 修改 `OpenApi.ts` <img src="https://pic.code-nav.cn/post_picture/1626606778294771714/lVH25KLP02rXtNQs.webp" alt="image.png" width="100%" /> 2. 运行 `build` 生成 `dist` 文件夹 <img src="https://pic.code-nav.cn/post_picture/1626606778294771714/xt3rfr2hjrOEC3qg.webp" alt="image.png" width="100%" /> 3. 编写 Dockerfile ```dockerfile FROM nginx WORKDIR /usr/share/nginx/html/ USER root COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf COPY ./dist /usr/share/nginx/html/ EXPOSE 8080 CMD ["nginx", "-g", "daemon off;"] ``` 4. 上传 dist 文件夹,dockerfile,docker文件夹 <img src="https://pic.code-nav.cn/post_picture/1626606778294771714/1AxLbVHNLFZFqnIQ.png" alt="image.png" width="274px" /> nginx.conf 文件内容如下: ```nginx server { listen 8080; # gzip config gzip on; gzip_min_length 1k; gzip_comp_level 9; gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml; gzip_vary on; gzip_disable "MSIE [1-6]\."; root /usr/share/nginx/html; include /etc/nginx/mime.types; location / { try_files $uri $uri /index.html; } } ``` 5. 执行命令 ```bash docker build -t oj-frontend . ``` ```bash docker run -d -p xxxx:xxxx --name oj-frontend oj-frontend ``` **注意端口号要和 dockerfile 的端口号一致** *** 到这里部署已经完成,**注意端口号。** ## 封面图 <img src="https://pic.code-nav.cn/post_picture/1626606778294771714/kC9WakhUmzgI0iAA.webp" alt="preview.jpg" width="100%" />
OJ项目部署上线笔记(前端+后端部署)
# **后端部署(yuoj-backend-microservice-master):** ### **1、使用的服务器规格说明:我使用的是2核4g的服务器,2g内存的服务器可能运行不起来,服务器系统使用的是centos 7.9; ssh连接工具是 MobaXterm** ### 2、后端部署可以先观看鱼皮的这个视频(看视频的时候先别操作,请看完下方部署注意事项后再操作): https://www.bilibili.com/video/BV1Cp4y1F7eA/?spm_id_from=333.337.search-card.all.click ### **3、部署注意事项:** ①在源码中的yuoj-backend-gateway模块中的Dockerfile文件中的EXPOSE 8104应该修改为EXPOSE 8101; ### **注:服务器的相关端口一定要开放!!!** ②为防止服务器的内存小导致项目无法成功启动,请将项目中所有的Dockerfile文件中的启动命令里添加上JVM的内存参数设置,如图所示: <img src="https://pic.code-nav.cn/post_picture/1808869779685212161/ucs0ehywTWrUUl2R.png" alt="image.png" width="100%" /> ③由于oj项目的远程代码沙箱是作为一个项目独立运行的,也需要打包上线,所以应该将yuoj-backend-judge-service模块中的RemoteCodeSandbox类里的executeCode方法中的url路径进行修改,将红框中的**localhost修改为服务器的ip地址**(这是偷懒的做法);(应该修改url,让其可以动态的读取application-prod.yml里的值,具体做法可以参考鱼皮视频里提及的InitRabbitMqBean类里的写法); ### **注意:一定要开放服务器的8090端口!!!** <img src="https://pic.code-nav.cn/post_picture/1808869779685212161/D3gp7H6bNmzVQuUn.webp" alt="QQ图片20240830202305.png" width="100%" /> ④使用docker compose命令一次性启动项目中所有模块的时候,可能会出现某个项目无法成功启动,此时我们可以使用docker compose -f docker- compose.service.yml up “模块名” -d 命令进行重新启动(就像鱼皮视频中演示的一样) **建议:使用docker compose -f docker-compose.service.yml up “模块名” -d 命令 一个模块一个模块地启动,防止某个模块因为内存分配不足导致启动失败。** **注:可以使用docker ps -a 命令查看容器是否都处于运行状态** **注:项目部署上线后,如果你在本地修改了某个模块的代码,要在本地重新打包,把jar包重新上传到服务器对应的位置上(即该模块在服务器上对应的目录的target目录里),然后要把该模块的容器停止了,然后删除该容器,然后再把该模块的镜像删除了,最后再使用docker compose命令重新构建容器** **⑤服务器上下载的maven版本要在3.1以上,因为远程代码沙箱项目在服务器上打包的话,需要服务器上的maven的版本在3.1以上。(请自行搜索linux系统上如何下载对应版本的maven)** # **后端部署(yuoj-code-sandbox-master):** ### **1、远程代码沙箱项目也是使用Docker部署的:** ①在服务器的code文件下创建和项目名相同的目录 即/code/yuoj-code-sandbox-master/ (如图) 将yuoj-code-sandbox-master项目中的文件从本地都上传到服务器的/code/yuoj-code-sandbox-master/ 目录下,如图。 而后可以使用mvn命令进行打包(如果服务器上使用mvn命令打包失败,可以在本地打好jar包后,将整个项目重新上传到服务器) 然后在yuoj-code-sandbox-master目录下创建Dockerfile文件(如图) <img src="https://pic.code-nav.cn/post_picture/1808869779685212161/GXZjA2HTFcQmdMhq.png" alt="image.png" width="266px" /> <img src="https://pic.code-nav.cn/post_picture/1808869779685212161/hbqCoiLuJppvYAjh.png" alt="image.png" width="260px" /> **Dockerfile文件里的内容:** ``` # 基础镜像 FROM openjdk:8-jdk-alpine # 将 jar 包添加到工作目录,比如 target/yuoj-backend-user-service-0.0.1-SNAPSHOT.jar ADD target/yuoj-code-sandbox-0.0.1-SNAPSHOT.jar . # 暴露端口 EXPOSE 8090 # 启动命令 ENTRYPOINT ["java","-Xms128m", "-Xmx256m","-jar","/yuoj-code-sandbox-0.0.1-SNAPSHOT.jar"] ``` ②在服务器的code文件夹下创建docker-compose.service.yml文件,如图: <img src="https://pic.code-nav.cn/post_picture/1808869779685212161/ouhePW86awYeFlGZ.png" alt="image.png" width="260px" /> docker-compose.service.yml文件里的内容如下: ``` version: '3' services: yuoj-code-sandbox-master: container_name: yuoj-code-sandbox-master build: context: ./yuoj-code-sandbox-master dockerfile: Dockerfile ports: - "8090:8090" networks: - mynetwork networks: mynetwork: ``` ③在code目录下使用docker compose命令,如下: ``` docker compose -f docker-compose.service.yml up yuoj-code-sandbox-master -d ``` ④使用docker ps -a 命令查看yuoj-code-sandbox-master容器是否启动成功,若不成功请重新执行步骤③ 使用docker ps -a 命令查看所有容器都正常运行则表明部署成功(也可以访问 ip:8101/doc.html 使用聚合文档验证)**注意该ip为你自己服务器的公网ip** # **前端部署(yuoj-frontend-master):** ### **1、在本地进入yuoj-frontend-master\generated\core里,点击OpenAPI.ts,修改OpenAPIConfig里的配置,如图:** **注:如果是使用域名访问,请将BASE里的地址修改为 http://域名 (例如:http://www.oj.com) ; 如果是使用服务器的ip访问,就将BASE里的localhost改为服务器的公网ip地址;此外还需将WITH_CREDENTIALS里的值改为true,如图:** <img src="https://pic.code-nav.cn/post_picture/1808869779685212161/b5kIGuc0Mj4QBG2O.webp" alt="image.png" width="100%" /> ### **2、在终端运行 npm run build命令将项目进行打包,然后生成一个dist文件夹** ### **3、在服务器的code目录下创建一个与前端项目同名的文件夹,即/code/yuoj-frontend-master/ 如图:** <img src="https://pic.code-nav.cn/post_picture/1808869779685212161/EO7u0texTHthYfZC.png" alt="image.png" width="263px" /> ### **4、将刚刚在本地生成的dist文件夹,上传到服务器的yuoj-frontend-master目录下,如图:** <img src="https://pic.code-nav.cn/post_picture/1808869779685212161/SLAJm2xZsqrYgyjv.png" alt="image.png" width="262px" /> ## **此时前后端项目都已经部署到服务器上,但是还需要nginx进行代理转发(请自行搜索Linux系统如何下载nginx)** **注意:一定要开放服务器的80端口** ### **1、找到nginx.conf,修改nginx.conf里server块的配置** **注意:如果nginx是使用yum命令下载的,server块在/etc/nginx/conf.d/下的default.conf文件里** **server块修改如下(填写完server_name那行的ip后,ip后面是有个 ; 的):** <img src="https://pic.code-nav.cn/post_picture/1808869779685212161/TSfhToSF0aVKprMG.webp" alt="image.png" width="100%" /> #### 注意:root后面的目录就是dist文件夹在服务器上的位置, **此外在location / 模块里还需添加try_files $uri $uri/ /index.html;** **2、在正确的目录下使用 ./nginx -s reload 命令(使用该命令前请确保nginx已经启动)重新加载nginx的配置文件(例如nginx是使用yum下载的,应在/usr/sbin目录下使用该命令)** **注意:如果使用./nginx -s reload 命令后报错,则按提示信息修改配置文件里的内容,然后再重新执行./nginx -s reload 命令** # **到此,oj项目前后端都部署完毕,在浏览器使用IP或者域名即可访问。**
用户中心,弄一个线上数据库——华为云使用
<html> <head></head> <body> <div class="content ql-editor"> <h2 class="ql-line-height-1_6"><strong style="color: rgb(51, 51, 51);">购买云服务器</strong></h2> <p><br></p> <p><a href="https://activity.huaweicloud.com/ecs.html" target="_blank" style="color: rgb(65, 131, 196);">https://activity.huaweicloud.com/ecs.html</a> 买了36/年的套餐,不知道算不算薅到羊毛</p> <p><span class="ql-font-monospace"> </span></p> <p>点【控制台】,点击【服务器】</p> <p><span class="ql-font-monospace"> </span></p> <p>【修改密码】【重启】</p> <p><br></p> <h2 class="ql-line-height-1_6"><strong style="color: rgb(51, 51, 51);">远程连接服务器 安装mysql</strong></h2> <p><br></p> <p>默认ubantu系统,但是里面默认了三个docker,其中一个里面安装了mariadb,占用了3306端口</p> <div class="ql-code-block-container"> <div class="ql-code-block"> # 先查看docker </div> <div class="ql-code-block"> sudo docker ps </div> </div> <p><span class="ql-font-monospace"> </span></p> <div class="ql-code-block-container"> <div class="ql-code-block"> # 停docker </div> <div class="ql-code-block"> sudo docker stop [CONTAINER ID] </div> <div class="ql-code-block"> </div> <div class="ql-code-block"> ## 安装mysql </div> <div class="ql-code-block"> # 更新软件包列表 </div> <div class="ql-code-block"> sudo apt update </div> <div class="ql-code-block"> # 查看可使用的安装包 </div> <div class="ql-code-block"> sudo apt search mysql-server </div> <div class="ql-code-block"> # 安装指定版本 </div> <div class="ql-code-block"> sudo apt install -y mysql-server-8.0 </div> </div> <p><span class="ql-font-monospace"> </span></p> <p><span class="ql-font-monospace"> </span></p> <div class="ql-code-block-container"> <div class="ql-code-block"> # 进入数据库 </div> <div class="ql-code-block"> mysqul -uroot -p </div> </div> <p><br></p> <h2 class="ql-line-height-1_6"><strong style="color: rgb(51, 51, 51);">授权外部访问</strong></h2> <h3 class="ql-line-height-1_6"><strong style="color: rgb(51, 51, 51);">配置服务器</strong></h3> <p><br></p> <p>ping服务器公网地址,如果ping不通就要检查安全组是否增加ICMP规则</p> <p><span class="ql-font-monospace"> </span></p> <p>新增规则,源地址填0.0.0.0/0 表示所有地址<span style="background-color: rgb(255, 255, 255); color: rgb(51, 51, 51);"><br></span></p> <p><span class="ql-font-monospace"> </span></p> <p>开放3306端口</p> <p><span class="ql-font-monospace"> </span></p> <p><br></p> <h3 class="ql-line-height-1_6"><strong style="color: rgb(51, 51, 51);">配置数据库</strong></h3> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> #新建用户 </div> <div class="ql-code-block"> #这里一定要用% 表示所有地址都能访问 </div> <div class="ql-code-block"> create user 'username'@'%' identified by 'password'; </div> <div class="ql-code-block"> </div> <div class="ql-code-block"> #授权用户 </div> <div class="ql-code-block"> #把所有数据库的所有表授权给所有ip地址的username用户 </div> <div class="ql-code-block"> grant privileges on *.* to username@'%' with grant option; </div> <div class="ql-code-block"> </div> <div class="ql-code-block"> #刷新生效权限 </div> <div class="ql-code-block"> flush privileges; </div> </div> <p>打开etc/mysql/mysqlid.conf.d文件夹下的mysqld.cnf文件,修改配置</p> <p>bind-address mysqlx-bind-address 都改成0.0.0.0,表示所有ip都能访问</p> <p><span class="ql-font-monospace"> </span></p> <p>防火墙开放3306端口</p> <div class="ql-code-block-container"> <div class="ql-code-block"> firewall-cmd --zone=public --add-port=3306/tcp --permanent </div> <div class="ql-code-block"> </div> <div class="ql-code-block"> #重载生效 </div> <div class="ql-code-block"> firewall-cmd --reload </div> <div class="ql-code-block"> #查看防火墙开放端口 </div> <div class="ql-code-block"> firewall-cmd --list-ports </div> </div> <p>检验是否能和端口进行会话</p> <p>打开powershell,注意不是cmd,</p> <div class="ql-code-block-container"> <div class="ql-code-block"> Test-NetConnection -ComputerName [你的公网地址] -Port 3306 </div> </div> <p><span class="ql-font-monospace"> </span></p> <p><br></p> <h2 class="ql-line-height-1_6"><strong style="color: rgb(51, 51, 51);">参考文章</strong></h2> <p><br></p> <p>编程导航用户中心沙鱼笔记-部署部分 <a href="https://www.yuque.com/moyan-awh3b/nccb2c/guu2d4#OwYu5" target="_blank" style="color: rgb(65, 131, 196);">https://www.yuque.com/moyan-awh3b/nccb2c/guu2d4#OwYu5</a></p> <p>华为云文档 <a href="https://support.huaweicloud.com/ecs_faq/zh-cn_topic_0105130172.html#ZH-CN_TOPIC_0105130172__section1715910911214" target="_blank" style="color: rgb(65, 131, 196);">https://support.huaweicloud.com/ecs_faq/zh-cn_topic_0105130172.html#ZH-CN_TOPIC_0105130172__section1715910911214</a></p> <p>ubuntu安装mysql <a href="https://blog.csdn.net/weixin_45626288/article/details/133220238" target="_blank" style="color: rgb(65, 131, 196);">https://blog.csdn.net/weixin_45626288/article/details/133220238</a></p> <p>我的csdn <a href="http://t.csdnimg.cn/U5uqn" target="_blank" style="background-color: rgb(245, 246, 247); color: rgb(0, 0, 0);">http://t.csdnimg.cn/U5uqn</a></p> </div> </body> </html>
智慧答 - lenyanAI答题应用平台 完结啦
<html> <head></head> <body> <div class="content ql-editor"> <h1>《智慧答 - lenyanAI答题应用平台》</h1> <p><br></p> <p>原文链接:<a href="https://nvb09f4g6y1.feishu.cn/wiki/PlDEww2yHioiiwkCKpoc40JFnqT" target="_blank">《智慧答 - lenyanAI答题应用平台》</a></p> <p><br></p> <p>历时一个多月,其实真正用了大约15天。</p> <p>暑假还做了其他事情,黑马的一个高级项目,鱼皮的聚合搜索+黑马的ES课程,每天都看八股。</p> <p>嘿嘿不知不觉暑假就结束了,现在来交个作业先咯。</p> <p>智慧答:AI答题平台,结合业务和流行的AI 独立开发的AI答题应用平台;</p> <p>从后端到前端,学会了许多。</p> <p><br></p> <h3>后端:(上线-宝塔)</h3> <ol> <li data-list="ordered"><span class="ql-ui"></span>后端的从搭建<strong>项目模块</strong>,到业务分析于开发,采用<strong>策略模式</strong>对评分模块的封装。</li> <li data-list="ordered"><span class="ql-ui"></span>接着引入<strong>智谱 AI</strong>,跟进AI时代。用AI生成题目,一步一步调Prompt,调bug。</li> <li data-list="ordered"><span class="ql-ui"></span>老生常谈的优化利用中间件,<strong>SSE</strong> + <strong>RxJava </strong>实现高性能的流式推送。</li> <li data-list="ordered"><span class="ql-ui"></span>采用<strong>Caffeine </strong>缓存多个用户的同一答案。<strong>分库分表</strong>减轻数据库的压力。</li> </ol> <p><br></p> <h3>小程序</h3> <p>(简简单单,虽说对于我拿过一个小程序的国奖,还是有训练价值)</p> <p><strong>Taro </strong>+ <strong>React </strong>新学的内容。发布有点难,就没发布了</p> <p><br></p> <h3>前端:(上线-宝塔)</h3> <ol> <li data-list="ordered"><span class="ql-ui"></span>鱼总的教程确实适合新手,像我连框架都没学,只学了三件套都学的来。</li> <li data-list="ordered"><span class="ql-ui"></span>后期结合AI和官方文档优化 嘿嘿。</li> <li data-list="ordered"><span class="ql-ui"></span>前端的逻辑,都理的清除,数据的传递和处理等等~</li> </ol> <p><br></p> <p>项目宝塔上线 部署及bug解决:</p> <p><a href="https://nvb09f4g6y1.feishu.cn/docx/DEPId3VvkokVnKxSSRScsNcznnb?from=from_copylink" target="_blank">《AI答题项目 上线过程Bug解决 (宝塔前后端)》</a></p> <p>《AI答题项目 上线过程Bug解决 (宝塔前后端)》</p> <p>最后也附上个仓库源码。 可以滴话,点个<span style="color: rgb(255, 102, 112);"> star</span>✨是对我最大的支持</p> <p><a href="https://github.com/lenyanjgk/lendada" target="_blank">https://github.com/lenyanjgk/lendada</a></p> <p>(lenyanjgk/lendada: 智慧答 - AI答题应用平台 (github.com))</p> <p><br></p> <p><br></p> <p>原文链接:<a href="https://nvb09f4g6y1.feishu.cn/wiki/PlDEww2yHioiiwkCKpoc40JFnqT" target="_blank"> 《智慧答 - lenyanAI答题应用平台》</a></p> <p>《智慧答 - lenyanAI答题应用平台》</p> <p><br></p> <p>智慧答(lendada)</p> <p>图片展示:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/mozlxd1p.jpeg"></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/0g7ozoz6.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/i0vsnk0e.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/v63rox2q.jpeg" style=""><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/afey59zv.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/s4f7iwvi.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/t2sb0x1y.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/z6e22u46.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/c5y4ifbx.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/uv3snqmu.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/du3dv9h7.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/zw5ewwae.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/qimv5ey0.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/tvsk21pb.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/1yp5mj9n.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/fk59gidv.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/zmrkvh92.jpeg"><img src="https://pic.code-nav.cn/planet_post_image/1738833787455823874/kzwfzlwr.jpeg"></p> </div> </body> </html>
用户中心项目 笔记
修复许多bug后,终于把用户中心做出来了,遇到的很多问题和解决方法都放在笔记里了,其中有几个问题影响深刻,拿出来说说。 1.前端app.tsx的白名单问题,因为白名单,总是无法跳转到欢迎页,后来改了原有的逻辑才成功跳转。会被这个问题卡住是因为我对ant design pro不熟悉,里面的函数有很多层调用,看了很久才了解一点逻辑。 2.部署时遇到的问题。 第一个是在云端部署的时候发现登录后跳转到欢迎页,头像不显示,也不能注销,后来查看头像显示的逻辑发现是没有用户名,因为注册的时候并没有填写用户名。 第二个是使用宝塔面板部署的时候,前端页面进不去,最后发现是项目目录设置错了,应该设置成dist,因为使用nginx部署前是要先build成dist,nginx才能找到index.html。 3.时效性 代码具有时效性,看视频的时候总有些东西和实操起来不同,这时候就需要查看最新的官方文档或者找比较新的博客。 前端:https://github.com/xiaoxiaoyeyuzhong/UserCenter_WebStorm_fdt 后端:https://github.com/xiaoxiaoyeyuzhong/user_center_backend_fdt
【用户匹配项目】学习过程遇到的坑
gson识别数据库里的字段必须按照设定的,传数组就是数组 比如["java","python"],这个是可以被直接识别且不会查询出错的 但是如果是"[java,python]",就会被判断称字符串,直接报错,前面自己测试sql模糊查询放了个这个,误导了内存查询的方法 com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was STRING at line 1 column 2 path $ 解决办法也有,强制修正一下,也能查出来这种异常情况,且不会报错 // 检查并修正字符串格式的JSON if (tagsStr.startsWith("\"[") && tagsStr.endsWith("]\"")) { tagsStr = tagsStr.substring(1, tagsStr.length() - 1).replace("\\\"", "\""); } Set<String> tempTagNameSet = gson.fromJson(tagsStr, new TypeToken<Set<String>>() {}.getType()); tempTagNameSet = Optional.ofNullable(tempTagNameSet).orElse(new HashSet<>());
