Merge branch 'feature/workflow' of http://116.204.74.41:3000/red-future/admin-ui into feature/workflow

This commit is contained in:
2026-05-26 10:11:26 +08:00
6 changed files with 321 additions and 28 deletions

View File

@@ -2,52 +2,67 @@ name: 全局K3s部署
on: on:
push: push:
branches: [master] branches: [master]
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest # ========== 核心修复替换为具体Ubuntu版本解决运行期匹配问题 ==========
runs-on: ubuntu-24.04
env: env:
K3S_HOST: 121.37.117.181 # 从组织级Secrets读取不用在仓库重复配置
K3S_HOST: ${{ secrets.K3S_HOST }}
APP_NAME: ${{ gitea.repo_name }} APP_NAME: ${{ gitea.repo_name }}
REGISTRY: 你的镜像仓库地址 # 比如 docker.io/你的用户名 # 补充若后续要推送镜像需替换为实际镜像仓库地址比如你的Gitea镜像仓库
REGISTRY: 116.204.74.41:3000/red-future
steps: steps:
- uses: gitea/actions/checkout@v4 # ========== 核心新增国内Git代理彻底解决GitHub拉取慢 ==========
- name: 配置国内GitHub代理加速
run: |
# 全局Git代理所有GitHub请求走国内镜像站
git config --global url."https://ghproxy.com/https://github.com/".insteadOf "https://github.com/"
# 可选替换Ubuntu源为清华源加速依赖安装
sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list
apt update -y
# ========== 核心修改替换checkout源避开GitHub ==========
- name: 拉取代码Gitea官方源
uses: gitea/actions/checkout@v4
with:
fetch-depth: 0 # 可选:拉取完整历史,加速后续操作
timeout-minutes: 10 # 增加超时,避免拉取中断
# 1. 初始化 Docker Buildx # 1. 初始化 Docker Buildx(原内容不变)
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
# 2. 登录镜像仓库(按需 # 2. 可选:登录镜像仓库(若需推送镜像,取消注释并配置密钥
- name: Login to DockerHub # - name: Login to Gitea Registry
uses: docker/login-action@v3 # uses: docker/login-action@v3
with: # with:
username: ${{ secrets.DOCKER_USER }} # registry: 116.204.74.41:3000
password: ${{ secrets.DOCKER_PWD }} # username: ${{ secrets.GITEA_USER }}
# password: ${{ secrets.GITEA_PWD }}
# 3. 构建+推送,启用缓存 # 3. 构建+推送(原内容不变)
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true
tags: ${{ env.REGISTRY }}/${{ env.APP_NAME }}:${{ gitea.sha }} tags: ${{ env.REGISTRY }}/${{ env.APP_NAME }}:${{ gitea.sha }}
# 缓存配置:推送到镜像仓库
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.APP_NAME }}:buildcache cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.APP_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.APP_NAME }}:buildcache,mode=max cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.APP_NAME }}:buildcache,mode=max
# 4. 核心修改先上传deploy.yaml到K3s服务器再执行kubectl # 4. 修复后的SSH部署步骤解决路径+命名空间问题)
- name: SSH部署K3s - name: SSH部署K3s
run: run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
echo "${{ secrets.K3S_PEM_KEY }}" > k3s.pem echo "${{ secrets.K3S_PEM_KEY }}" > k3s.pem
chmod 600 k3s.pem chmod 600 k3s.pem
# 关键1把Gitea仓库里的deploy.yaml上传到K3s服务器临时目录/tmp
# 注意如果你的deploy.yaml不在仓库根目录要修改./deploy.yaml为实际路径 # ========== 修正1上传仓库根目录的deploy.yaml到K3s临时目录 ==========
scp -i k3s.pem -o StrictHostKeyChecking=no ./deploy.yaml root@${K3S_HOST}:/tmp/ scp -i k3s.pem -o StrictHostKeyChecking=no ./deploy.yaml root@${K3S_HOST}:/tmp/
# 关键2执行kubectl时指向临时目录的文件而非不存在的/k8s/
# ========== 修正2kubectl指向临时文件+补充命名空间 ==========
ssh -i k3s.pem -o StrictHostKeyChecking=no root@${K3S_HOST} << CMD ssh -i k3s.pem -o StrictHostKeyChecking=no root@${K3S_HOST} << CMD
kubectl apply -f /tmp/deploy.yaml kubectl apply -f /tmp/deploy.yaml
kubectl rollout restart deployment ${APP_NAME} -n default kubectl rollout restart deployment ${APP_NAME} -n default
# 可选:部署完成后删除临时文件,清理服务器 rm -f /tmp/deploy.yaml # 可选:清理临时文件
rm -f /tmp/deploy.yaml
CMD CMD

View File

@@ -1,12 +1,6 @@
FROM gitea/gitea:latest
# 拷贝预设工作流模板到容器内仓库模板目录
COPY ./workflow_template/.gitea /data/gitea/templates/repo/.gitea
# ==================== 第一阶段:构建前端 ==================== # ==================== 第一阶段:构建前端 ====================
FROM node:18-alpine AS builder FROM node:18-alpine AS builder
WORKDIR /app WORKDIR /app
# 配置Alpine国内镜像源 # 配置Alpine国内镜像源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
@@ -30,6 +24,6 @@ COPY ngnix.conf /etc/nginx/conf.d/default.conf
# 复制SSL证书 # 复制SSL证书
COPY ssl/* /etc/nginx/ssl/ COPY ssl/* /etc/nginx/ssl/
EXPOSE 443 EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

44
deploy.yaml Normal file
View File

@@ -0,0 +1,44 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${APP_NAME}
namespace: default
labels:
app: ${APP_NAME}
spec:
replicas: 1
selector:
matchLabels:
app: ${APP_NAME}
template:
metadata:
labels:
app: ${APP_NAME}
spec:
containers:
- name: ${APP_NAME}
image: ${REGISTRY}/${APP_NAME}:${gitea.sha}
imagePullPolicy: Always
ports:
- containerPort: 80 # 你的项目实际端口比如前端80、后端8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: ${APP_NAME}-service
namespace: default
spec:
type: NodePort
selector:
app: ${APP_NAME}
ports:
- port: 80
targetPort: 80
nodePort: 30001 # 必须在30000-32767之间

View File

@@ -1,5 +1,12 @@
# Nginx 静态文件服务 + 智能代理 # Nginx 静态文件服务 + 智能代理
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server { server {
# 静态资源根目录dist # 静态资源根目录dist
root /usr/share/nginx/html; root /usr/share/nginx/html;

View File

@@ -0,0 +1,16 @@
import request from '/@/utils/request';
export function getPwConfig() {
return request({
url: '/admin-go/api/v1/system/pwconfig/get',
method: 'get',
});
}
export function savePwConfig(data: any) {
return request({
url: '/admin-go/api/v1/system/pwconfig/save',
method: 'post',
data: data,
});
}

View File

@@ -0,0 +1,217 @@
<template>
<div class="system-pwconfig-container">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>密码策略配置</span>
</div>
</template>
<el-form :model="ruleForm" ref="formRef" :rules="rules" label-width="180px" style="max-width: 800px;">
<el-form-item label="启用密码策略" prop="enabled">
<el-switch v-model="ruleForm.enabled" />
</el-form-item>
<el-form-item label="最小密码长度" prop="minLength">
<el-input-number v-model="ruleForm.minLength" :min="4" :max="32" placeholder="请输入最小密码长度" />
<span class="ml10 text-muted"></span>
</el-form-item>
<el-form-item label="最大密码长度" prop="maxLength">
<el-input-number v-model="ruleForm.maxLength" :min="4" :max="128" placeholder="请输入最大密码长度" />
<span class="ml10 text-muted"></span>
</el-form-item>
<el-form-item label="必须包含大写字母" prop="requireUppercase">
<el-switch v-model="ruleForm.requireUppercase" />
</el-form-item>
<el-form-item label="必须包含小写字母" prop="requireLowercase">
<el-switch v-model="ruleForm.requireLowercase" />
</el-form-item>
<el-form-item label="必须包含数字" prop="requireDigit">
<el-switch v-model="ruleForm.requireDigit" />
</el-form-item>
<el-form-item label="必须包含特殊字符" prop="requireSpecialChar">
<el-switch v-model="ruleForm.requireSpecialChar" />
<div class="text-muted mt5" style="font-size: 12px;">特殊字符包括!@#$%^&*()_+-=[]{}|;:,.<>?</div>
</el-form-item>
<el-form-item label="密码过期天数" prop="expireDays">
<el-input-number v-model="ruleForm.expireDays" :min="0" :max="365" placeholder="请输入密码过期天数" />
<span class="ml10 text-muted">0表示永不过期</span>
</el-form-item>
<el-form-item label="禁止重复使用次数" prop="historyLimit">
<el-input-number v-model="ruleForm.historyLimit" :min="0" :max="24" placeholder="请输入禁止重复使用次数" />
<span class="ml10 text-muted">0表示不限制</span>
</el-form-item>
<el-form-item label="登录失败锁定次数" prop="maxRetryCount">
<el-input-number v-model="ruleForm.maxRetryCount" :min="0" :max="10" placeholder="请输入登录失败锁定次数" />
<span class="ml10 text-muted">0表示不锁定</span>
</el-form-item>
<el-form-item label="锁定时长" prop="lockTimeMinutes">
<el-input-number v-model="ruleForm.lockTimeMinutes" :min="1" :max="1440" placeholder="请输入锁定时长" />
<span class="ml10 text-muted">分钟</span>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="ruleForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" :loading="loading">保存配置</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script lang="ts">
import { reactive, toRefs, defineComponent, ref, onMounted, unref } from 'vue';
import { ElMessage, FormInstance, FormRules } from 'element-plus';
import { getPwConfig, savePwConfig } from "/@/api/system/pwconfig";
interface RuleFormState {
enabled: boolean;
minLength: number;
maxLength: number;
requireUppercase: boolean;
requireLowercase: boolean;
requireDigit: boolean;
requireSpecialChar: boolean;
expireDays: number;
historyLimit: number;
maxRetryCount: number;
lockTimeMinutes: number;
remark: string;
}
interface PwConfigState {
ruleForm: RuleFormState;
rules: FormRules;
loading: boolean;
}
export default defineComponent({
name: 'systemPwConfig',
setup() {
const formRef = ref<FormInstance>();
const state = reactive<PwConfigState>({
loading: false,
ruleForm: {
enabled: false,
minLength: 8,
maxLength: 32,
requireUppercase: false,
requireLowercase: true,
requireDigit: true,
requireSpecialChar: false,
expireDays: 90,
historyLimit: 5,
maxRetryCount: 5,
lockTimeMinutes: 30,
remark: '',
},
rules: {
minLength: [
{ required: true, message: '请输入最小密码长度', trigger: 'blur' }
],
maxLength: [
{ required: true, message: '请输入最大密码长度', trigger: 'blur' }
],
expireDays: [
{ required: true, message: '请输入密码过期天数', trigger: 'blur' }
],
historyLimit: [
{ required: true, message: '请输入禁止重复使用次数', trigger: 'blur' }
],
maxRetryCount: [
{ required: true, message: '请输入登录失败锁定次数', trigger: 'blur' }
],
lockTimeMinutes: [
{ required: true, message: '请输入锁定时长', trigger: 'blur' }
],
}
});
// 获取配置
const loadConfig = async () => {
try {
const res: any = await getPwConfig();
if (res.code === 0 && res.data) {
state.ruleForm = {
...state.ruleForm,
...res.data,
};
}
} catch {
// 错误由全局拦截器处理
}
};
// 重置表单
const resetForm = () => {
loadConfig();
};
// 保存配置
const onSubmit = async () => {
const formWrap = unref(formRef);
if (!formWrap) return;
await formWrap.validate(async (valid: boolean) => {
if (valid) {
// 验证最小长度不大于最大长度
if (state.ruleForm.minLength > state.ruleForm.maxLength) {
ElMessage.error('最小密码长度不能大于最大密码长度');
return;
}
state.loading = true;
try {
await savePwConfig(state.ruleForm);
ElMessage.success('保存成功');
} finally {
state.loading = false;
}
}
});
};
// 页面加载时
onMounted(() => {
loadConfig();
});
return {
formRef,
onSubmit,
resetForm,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.text-muted {
color: var(--el-text-color-placeholder);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.system-pwconfig-container {
:deep(.el-card__body) {
padding-top: 30px;
}
}
</style>