Write the Code. Change the World.

9月 10
精选文章

该后台使用 vite + ts + pnpm + vue3 + element-plus + tailwindcss 等技术栈构成。没有添加任意可视化图标等插件。以最小功能,最基础功能展现。用户可以额外添加可使用的插件逻辑。

该后台后端使用 php8.2 + laravel 10 + mysql

该后台后端 go 语言版本开发中。将使用 gframe2.5.2

源码: https://github.com/vini123/simpleAdmin

在线体验: https://www.zeipan.com/admin

权限以及密码一键复位: https://v3test.yuepaibao.com/admin/api/reset

测试账号以及密码: zhoulin@xiangrong.pro、 111111 (如果发现登录不了,可一键复位谢谢)

阅读全文 >>

8月 04

即使如此,也只能在局域网使用。如果想广域网使用,还得拆分。将 srs 服务端部署到广域网,局域网拉取其他 rtmp 流,推送到服务端。广域网才能使用。 项目需要,还得将这个直播功能在其他广域网 web 中使用。

阅读全文 >>

8月 04

现在更狠,要求服务端拉前端的流,还不告诉地址。还得让服务端通过前端广播的 udp 消息来确定拉流地址,然后进行推流处理。

修改后,代码结构如下:

pico rtmp 推流/
├── .env                          # 部署配置(Pico 地址 + CANDIDATE)
├── .gitignore                    ← 新增:忽略缓存/IDE/OS 残留
├── docker-compose.yaml           # srs + puller + controller 三服务
├── index.html                    # H5 播放页
├── srs.conf                      # SRS 配置
└── src/controller/
    ├── controller.py             # UDP 监听 + 健康检查 + 写共享地址
    ├── puller-wrapper.sh         # 监控地址变化、重启 ffmpeg
    ├── start-controller.ps1      # 原生启动脚本(备用)
    └── test-broadcast.py         # 广播模拟测试工具

阅读全文 >>

8月 04

现在是客户端不推流到服务端,反而是服务端拉取客户端的流,再分发出去。

https://blog.vini123.com/1414

上边这个链接客户端推流模式,这个简单,实用。但有时候,却说推不了流,只能被动拉流。 这个会增加复杂度,使用范围变成了局域网。

开始

文件目录:

├── .env                 # 配置文件
├── srs.conf             # SRS 5.x:RTMP 接流 + HLS/HTTP-FLV/WebRTC 三协议输出
├── docker-compose.yaml  # 一键启动,CANDIDATE 环境变量注入 WebRTC 候选 IP
└── index.html           # H5 播放页:WebRTC 优先 → HLS 兜底,断线自动重试

阅读全文 >>

8月 04

客户端向服务端推流,前端 h5 播放流。

https://ossrs.io/lts/zh-cn/

SRS(Simple Realtime Server)是一个简单高效的实时视频服务器,支持RTMP、WebRTC、HLS、HTTP-FLV、SRT等多种实时流媒体协议。Oryx是一个一体化、开箱即用、开源的视频解决方案,可部署在云上或自建机房,以直播和WebRTC等能力赋能你的业务。

阅读全文 >>

8月 03

php artisan tinker; 使用时,报错 Error Class "Psy\Readline\Interactive\Helper\TokenHelper" not found. 处理。

https://packagist.org/packages/psy/psysh

psy 版本:Psy Shell v0.12.24

root@a2129439b046:/www/tcrj.laravel# php artisan tinker;
Psy Shell v0.12.24 (PHP 8.5.5 — cli) by Justin Hileman
New PHP manual is available (latest: 3.1.1). Update with `doc --update-manual`

tinker 版本:

root@a2129439b046:/www/tcrj.laravel# composer show laravel/tinker
name : laravel/tinker
descrip. : Powerful REPL for the Laravel framework.
keywords : REPL, Tinker, laravel, psysh
versions : * v3.0.2
released : 2026-03-17, 4 months ago
type : library
license : MIT License (MIT) (OSI approved) https://spdx.org/licenses/MIT.html#licenseText
homepage :
source : [git] https://github.com/laravel/tinker.git 4faba77764bd33411735936acdf30446d058c78b
dist : [zip] https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b 4faba77764bd33411735936acdf30446d058c78b
path : /www/tcrj.laravel/vendor/laravel/tinker
names : laravel/tinker

进入 vendor 中,确实没看到 TokenHelper 这个类。手动去官网复制一份下来。
https://github.com/bobthecow/psysh/blob/main/src/Readline/Interactive/Helper/TokenHelper.php

进入到 vendor 目录,创建 TokenHelper.php 文件,复制代码进去。

<?php

/*
 * This file is part of Psy Shell.
 *
 * (c) 2012-2026 Justin Hileman
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Psy\Readline\Interactive\Helper;

/**
 * Shared token analysis utilities.
 */
class TokenHelper
{
    /** @var int[] Token types that indicate a statement continues on the next line */
    private const TRAILING_TOKEN_OPS = [
        \T_OBJECT_OPERATOR, \T_PAAMAYIM_NEKUDOTAYIM,
        \T_BOOLEAN_AND, \T_BOOLEAN_OR, \T_LOGICAL_AND, \T_LOGICAL_OR,
        \T_DOUBLE_ARROW, \T_COALESCE, \T_SPACESHIP,
    ];

    /** @var string[] Single-character operators that indicate continuation */
    private const TRAILING_CHAR_OPS = ['+', '-', '*', '/', '%', '.', '=', '&', '|', '^', '<', '>', ','];

    /**
     * Check whether the last non-whitespace token is a trailing operator.
     *
     * Used to determine whether a statement continues onto the next line.
     *
     * @param array $tokens token_get_all tokens
     */
    public static function hasTrailingOperator(array $tokens): bool
    {
        if (empty($tokens)) {
            return false;
        }

        $lastToken = null;
        for ($i = \count($tokens) - 1; $i >= 0; $i--) {
            $token = $tokens[$i];
            if (\is_array($token) && $token[0] === \T_WHITESPACE) {
                continue;
            }

            $lastToken = $token;
            break;
        }

        if ($lastToken === null) {
            return false;
        }

        if (\is_array($lastToken)) {
            return \in_array($lastToken[0], self::TRAILING_TOKEN_OPS, true);
        }

        return \in_array($lastToken, self::TRAILING_CHAR_OPS, true);
    }
}

再重启 composer。

composer dump-autoload -o

再使用 tinker 就可以了。

阅读全文 >>

7月 28

现在有这样一个需求。本地推送前端代码到 codeberg 后,服务端拉取代码,打包后,放到对应的位置。

服务器服务,全是由 docker 构建,文件复制移动全靠 volume 。

构建 api 接口服务

新建 webhook-server 目录,里边有下边几个文件。

.
├── Dockerfile
├── package.json
└── server.js

Dockerfile

FROM node:latest

WORKDIR /app

# 新增:安装docker客户端
RUN apt-get update && apt-get install -y ca-certificates curl \
    && curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/static/stable/x86_64/docker-27.3.1.tgz \
    | tar -xz -C /usr/local/bin --strip-components=1 docker/docker \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

COPY package.json ./
RUN npm install --omit=dev

COPY server.js ./

EXPOSE 9000
CMD ["node", "server.js"]

package.json

{
  "name": "webhook-server",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "express": "^5.0"
  }
}

server.js

/**
 * Webhook 接收服务
 *
 * 接收 Codeberg (Gitea) 的 push 通知,验证签名后触发 deploy.sh
 */
const express = require('express');
const crypto = require('crypto');
const { spawn } = require('child_process');

const PORT = parseInt(process.env.WEBHOOK_PORT || '9000', 10);
const SECRET = process.env.WEBHOOK_SECRET || '';
const DEPLOY_SCRIPT = process.env.DEPLOY_SCRIPT || '/opt/frontend-cicd/deploy.sh';
const TARGET_BRANCH = `refs/heads/${process.env.BRANCH || 'main'}`;

if (!SECRET || SECRET === 'CHANGE_ME') {
  console.error('[ERROR] 请先设置 WEBHOOK_SECRET');
  process.exit(1);
}

const app = express();
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook/deploy', (req, res) => {
  const body = req.body.toString('utf8');
  const sig = req.headers['x-gitea-signature'] || '';
  const event = req.headers['x-gitea-event'] || '';

  // 验证 HMAC-SHA256 签名
  const expected = crypto.createHmac('sha256', SECRET).update(body).digest('hex');
  if (sig !== expected) {
    console.warn(`[${ts()}] 签名验证失败`);
    return res.status(403).json({ error: 'Invalid signature' });
  }

  if (event !== 'push') return res.json({ msg: `忽略事件: ${event}` });

  let payload;
  try { payload = JSON.parse(body); } catch { return res.status(400).end(); }

  const ref = payload.ref || '';
  if (ref !== TARGET_BRANCH) return res.json({ msg: `忽略分支: ${ref}` });

  const pusher = payload.pusher?.login || '?';
  const commits = (payload.commits || []).length;
  console.log(`[${ts()}] ${pusher} 推送了 ${commits} 个提交到 ${ref},开始部署...`);

  // 先返回 200,避免 Codeberg 超时
  res.json({ msg: 'deploy triggered' });

        console.log(`[${ts()}] 开始执行部署脚本: ${DEPLOY_SCRIPT}`);
        const child = spawn('bash', [DEPLOY_SCRIPT], { timeout: 600000 });

        child.stdout.on('data', (data) => {
          console.log(`[${ts()}] [部署stdout] ${data.toString()}`);
        });
        child.stderr.on('data', (data) => {
          console.error(`[${ts()}] [部署stderr] ${data.toString()}`);
        });

        child.on('close', (code) => {
          if (code === 0) {
            console.log(`[${ts()}] 部署完成,退出码:${code}`);
          } else {
            console.error(`[${ts()}] 部署失败,脚本退出码:${code}`);
          }
        });

        child.on('error', (err) => {
          console.error(`[${ts()}] 启动脚本异常:`, err);
        });
});

app.get('/health', (_, res) => {
  res.json({ ok: true, uptime: process.uptime(), time: new Date().toISOString() });
});

app.listen(PORT, () => {
  console.log(`Webhook 服务已启动,端口 ${PORT}`);
  console.log(`  POST /webhook/deploy  (分支: ${TARGET_BRANCH})`);
  console.log(`  GET  /health`);
});

function ts() { return new Date().toISOString(); }

运行 api 接口服务

这里是完整的目录和文件。

.
├── deploy.sh
├── docker-compose.yml
├── setup.sh
└── webhook-server
    ├── Dockerfile
    ├── package.json
    └── server.js

deploy.sh

#!/bin/bash
# ============================================================
# 自动部署脚本 —— 兼容 webhook docker 容器运行
# ============================================================
set -euo pipefail

# ======================== 配置(修改路径!) ========================
PROJECT_DIR="/alidata/code/editor.manhuariji.com"
DEPLOY_DIR="/alidata/www/editor.manhuariji.com/public"
DOCKER_IMAGE="xr-node:20"
CONTAINER_NAME="editor-mhrj"
BUILD_CMD="npm run build:h5"
BUILD_OUTPUT="dist"
DETAIL_OUTPUT="build/h5"
BRANCH="main"
# 【修改点1】日志放到容器内/app目录,拥有写入权限!
LOG_FILE="/app/editor-mhrj-deploy.log"
LOCK_FILE="/tmp/editor-mhrj-deploy.lock"
# =====================================================

# ---- 防并发 ----
if [ -f "$LOCK_FILE" ]; then
  echo "$(date '+%Y-%m-%d %H:%M:%S') [WARN] 有部署正在运行,跳过" >> "$LOG_FILE"
  exit 0
fi
touch "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT

# log函数:同时输出到日志文件 AND 控制台(调试关键!)
log() {
    local msg="$(date '+%Y-%m-%d %H:%M:%S') $1"
    echo "$msg" >> "$LOG_FILE"
    echo "$msg"   # 打印到终端,Node服务可以捕获
}

log "========================================"
log "开始部署"
log "========================================"

# ---- 1. Git 拉取代码 ----
log "[1/3] cd $PROJECT_DIR && git pull ..."
cd "$PROJECT_DIR"

PREV=$(git rev-parse --short HEAD 2>/dev/null || echo "none")

# 强制同步远端代码,丢弃本地所有修改(适合自动部署服务器)
log "[INFO] 重置本地代码,对齐远端origin/${BRANCH}"
git fetch origin "${BRANCH}" >> "$LOG_FILE" 2>&1
git reset --hard "origin/${BRANCH}" >> "$LOG_FILE" 2>&1

NEW=$(git rev-parse --short HEAD)

#if [ "$PREV" = "$NEW" ]; then
#   log "没有新提交 ($NEW),跳过构建"
#   exit 0
# fi

log "代码更新: $PREV -> $NEW"

# ---- 2. Docker 容器内构建 ----
# 新增:清理宿主机缓存,腾出内存
log "[INFO] 清理系统缓存释放内存"
sync; echo 3 > /proc/sys/vm/drop_caches

log "[2/3] Docker 容器构建 ($DOCKER_IMAGE) ..."
log "  npm install + $BUILD_CMD"

docker run --rm \
  --memory=2G \
  --memory-swap=3G \
  --name "$CONTAINER_NAME" \
  -v "${PROJECT_DIR}":/www \
  -w /www \
  "$DOCKER_IMAGE" \
  bash -c "
    npm config set registry https://registry.npmmirror.com
    npm install
    NODE_OPTIONS='--max-old-space-size=1400' npm run build:h5
  " >> "$LOG_FILE" 2>&1

if [ $? -ne 0 ]; then
  log "[ERROR] 构建失败!"
  exit 1
fi

if [ ! -d "${PROJECT_DIR}/${BUILD_OUTPUT}" ]; then
  log "[ERROR] 构建产物目录 '${BUILD_OUTPUT}' 不存在"
  exit 1
fi

log "构建完成,产物: ${PROJECT_DIR}/${BUILD_OUTPUT}"

# ---- 3. 部署 ----
log "[3/3] 部署到 $DEPLOY_DIR ..."

# 备份逻辑保留(可选)
#if [ -d "$DEPLOY_DIR" ] && [ "$(ls -A "$DEPLOY_DIR" 2>/dev/null)" ]; then
#  BACKUP="${DEPLOY_DIR}.backup"
#  rm -rf "$BACKUP"
#  mv "$DEPLOY_DIR" "$BACKUP"
#  log "旧版本备份到 $BACKUP"
#fi

mkdir -p "$DEPLOY_DIR"
SRC="${PROJECT_DIR}/${BUILD_OUTPUT}/${DETAIL_OUTPUT}"

# 使用 cp -r 源头目录/. 方案,完美规避所有通配符问题
if [ -d "$SRC" ]; then
  cp -r "${SRC}/." "${DEPLOY_DIR}/"
else
  log "[ERROR] 构建目录不存在: $SRC"
  exit 1
fi

log "========================================"
log "部署完成!($PREV -> $NEW)"
log "========================================"

# ---- 日志截断 ----
MAX_LINES=3000
if [ -f "$LOG_FILE" ]; then
  LINES=$(wc -l < "$LOG_FILE")
  if [ "$LINES" -gt "$MAX_LINES" ]; then
    tail -n "$MAX_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp"
    mv "${LOG_FILE}.tmp" "$LOG_FILE"
  fi
fi

bash.sh

#!/bin/bash
# ============================================================
# 一键初始化脚本
#
# 用法:
#   1. scp -r frontend-cicd root@服务器IP:/opt/
#   2. 编辑 /opt/frontend-cicd/.env(填仓库地址和 Secret)
#   3. bash /opt/frontend-cicd/setup.sh
# ============================================================
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/.env"

echo "=========================================="
echo " editor.manhuariji.com 自动部署初始化"
echo "=========================================="

# ---- 1. 检查环境 ----
echo ""
echo "[1/5] 检查环境 ..."
for cmd in git docker; do
  if ! command -v $cmd &>/dev/null; then
    echo "[ERROR] $cmd 未安装"
    exit 1
  fi
done
echo "  git:    $(git --version)"
echo "  docker: $(docker --version)"

if [ "$WEBHOOK_SECRET" = "CHANGE_ME" ]; then
  echo ""
  echo "[ERROR] 请先编辑 .env,把 WEBHOOK_SECRET 改成随机字符串"
  echo "  生成方法: openssl rand -hex 24"
  exit 1
fi

# ---- 2. 克隆项目 ----
echo ""
echo "[2/5] 项目代码 ..."
if [ -d "$PROJECT_DIR/.git" ]; then
  echo "  已存在,git pull ..."
  cd "$PROJECT_DIR" && git pull
else
  echo "  克隆 $REPO_URL ..."
  mkdir -p "$(dirname "$PROJECT_DIR")"
  git clone "$REPO_URL" "$PROJECT_DIR"
fi

# ---- 3. 首次构建验证 ----
echo ""
echo "[3/5] 首次构建验证 ..."
cd "$PROJECT_DIR"

docker run --rm \
  --name "$CONTAINER_NAME" \
  -v "${PROJECT_DIR}":/www \
  -w /www \
  "$DOCKER_IMAGE" \
  bash -c "npm install && $BUILD_CMD"

if [ -d "$BUILD_OUTPUT" ]; then
  mkdir -p "$(dirname "$DEPLOY_DIR")"
  rm -rf "$DEPLOY_DIR"
  mv "$BUILD_OUTPUT" "$DEPLOY_DIR"
  echo "  首次部署完成 -> $DEPLOY_DIR"
else
  echo "[WARN] 构建产物不存在,跳过部署"
fi

# ---- 4. 准备 Webhook 镜像依赖 ----
echo ""
echo "[4/5] 准备 Webhook 镜像 ..."
# 把宿主机的 docker CLI 复制到 webhook-server 目录,Dockerfile 会 COPY 进去
# 这样构建镜像时不需要拉任何外部镜像
cp "$(which docker)" "$SCRIPT_DIR/webhook-server/docker"
echo "  已复制 docker CLI -> webhook-server/docker"

# ---- 5. 启动 Webhook ----
echo ""
echo "[5/5] 启动 Webhook 服务 ..."
cd "$SCRIPT_DIR"
docker compose up -d --build

echo ""
echo "=========================================="
echo " 初始化完成!"
echo "=========================================="
echo ""
echo " Webhook: http://服务器IP:$WEBHOOK_PORT/webhook/deploy"
echo " Secret:  $WEBHOOK_SECRET"
echo ""
echo " 验证:"
echo "   curl http://localhost:$WEBHOOK_PORT/health"
echo "   docker logs editor-mhrj-webhook"
echo ""
echo " 下一步:"
echo "   1. 阿里云安全组放行 $WEBHOOK_PORT 端口"
echo "   2. Codeberg → Settings → Webhooks → Add Webhook (Gitea)"
echo "      URL:    http://你的服务器IP:$WEBHOOK_PORT/webhook/deploy"
echo "      Secret: $WEBHOOK_SECRET"
echo "      Event:  Push Events"
echo "   3. 本地 git push 验证"
echo ""

docker-compose.yaml

"docker-compose.yml" 25L, 851C                                                                                                                                                                                                              7,5          全部
services:
  webhook-server:
    build: ./webhook-server
    container_name: editor-mhrj-webhook
    restart: always
    privileged: true
    volumes:
      - ./webhook-server/server.js:/app/server.js
      # 挂载 docker.sock,让容器能调用宿主机的 docker 执行 deploy.sh
      - /var/run/docker.sock:/var/run/docker.sock
      - /root/.ssh:/root/.ssh:ro
      # 挂载部署脚本(只读)
      - ./deploy.sh:/app/deploy.sh:ro
      - /alidata/code/editor.manhuariji.com:/alidata/code/editor.manhuariji.com
      - /alidata/www/editor.manhuariji.com/public:/alidata/www/editor.manhuariji.com/public
    environment:
      - WEBHOOK_SECRET=${WEBHOOK_SECRET}
      - WEBHOOK_PORT=${WEBHOOK_PORT:-9000}
      - BRANCH=${BRANCH:-main}
      - DEPLOY_SCRIPT=/app/deploy.sh
    networks:
      - xrnet
networks:
  xrnet:
    external: true

.env

# ======================== 按需修改 ========================
# Codeberg 仓库 SSH 地址(仅 setup.sh 首次 clone 用,deploy.sh 走已有的 remote)
REPO_URL=git@codeberg.org:manhuariji/manhuariji.h5.git

# 项目在宿主机上的路径
PROJECT_DIR=/alidata/code/editor.manhuariji.com

# 部署目标(Nginx public 目录)
DEPLOY_DIR=/alidata/www/editor.manhuariji.com/public

# 构建用的 Docker 镜像(你自己的镜像)
DOCKER_IMAGE=xr-node:20

# 容器名
CONTAINER_NAME=editor-mhrj

# 构建命令
BUILD_CMD=npm run build:h5

# 构建产物目录名
BUILD_OUTPUT=dist

# 打包输出详情目录
DETAIL_OUTPUT=build/h5/*

# 部署分支
BRANCH=main

# ---- WEBHOOK SECRET(和 git 无关!)----
# 这是 Codeberg 和你的 webhook 服务之间的"暗号",用来验证 HTTP 请求确实来自 Codeberg。
# 防止别人往你的 webhook 地址随便发 POST 触发部署。
# 生成方法:openssl rand -hex 24
# 这个值要和 Codeberg Webhook 设置页填的 Secret 一致。
# WEBHOOK_SECRET=CHANGE_ME
WEBHOOK_SECRET=b85cb03fd42507f5f921ac4534e76155c0045aed49d0236a

# Webhook 监听端口
WEBHOOK_PORT=9000

运行起来: docker compose up -d

nginx 代理

    location /cicd/ {
        proxy_pass http://webhook-server:9000/; 

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host:$server_port;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Context /cicd;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_connect_timeout 60s;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }

    location = /cicd {
        return 301 /cicd/;
    }

然后通过域名就可以开始部署了: https://xxx.com/cicd/webhook/deploy

codeberg 配置 Forgejo

登录 codeberg,进入项目,点击设置,切换到 web钩子页签。再点击添加web钩子, 选择 Forgejo。开始进行配置。

密钥文件和服务器上的文件一致哈。

可以点击 测试推送进行测试,也可以主动推送代码进行测试。

阅读全文 >>

7月 28

spatie/laravel-backup 框架不仅可以备份数据库,还能方便的备份文件,以及通知。

https://github.com/spatie/laravel-backup

https://spatie.be/docs/laravel-backup/v10/introduction

安装

composer require spatie/laravel-backup

php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider" --tag=backup-config

备份相关的配置文件在 config/backup.php 里。可以根据自己的情况调整处理。

如果是使用 php 镜像运行的项目,可能会出现下边这样的错误。

Starting backup...\nDumping database test_holovision...\nBackup failed because: The dump process failed with a none successful exitcode.\nExitcode\n========\n127: Command not found\n\nOutput\n======\n\n\nError Output\n============\nsh: 1: mysqldump: not found\n.\n#0 /www/www.holovision.com/vendor/spatie/db-dumper/src/DbDumper.php(200):

就是 mysqldump 不存在。这个需要在镜像里单独安装。推荐装 mariadb 的,不装 mysql 的。

可以进入容器,在容器中安装完成测试。

apt-get update

apt-get install -y --no-install-recommends mariadb-client 

rm -rf /var/lib/apt/lists/

确定 ok 的话,可以在构建 php 镜像时,加入 mysqldump 的安装的部分。

# 安装 mariadb-client(内含 mysqldump)
RUN apt-get update && apt-get install -y --no-install-recommends \
    mariadb-client \
    && rm -rf /var/lib/apt/lists/*

通过定时任务来进行备份

  1. 新建 BackupCommand .

    php artisan make:command BackupCommand
  2. 填入代码

    option('force');
    
        $backupSetting = DatabaseBackupSetting::first();
        if (!$backupSetting) {
            Log::channel('backup')->error('备份设置未配置');
            return;
        }
    
        if (!$backupSetting->is_enable && !$force) {
            Log::channel('backup')->error('备份设置已关闭:' . $force);
            return;
        }
    
        $last_backup_time = $backupSetting->last_backup_time;
        if ($last_backup_time) {
            $need_backup = false;
            if ($backupSetting->backup_interval == 1) {
                // 按天进行备份
                if (Carbon::parse($last_backup_time)->addDays(1)->addMinutes(-2)->lt(Carbon::now())) {
                    $need_backup = true;
                }
            } elseif ($backupSetting->backup_interval == 2) {
                // 按周进行备份
                if (Carbon::parse($last_backup_time)->addweeks(1)->addMinutes(-2)->lt(Carbon::now())) {
                    $need_backup = true;
                }
            } elseif ($backupSetting->backup_interval == 3) {
                // 按月进行备份
                if (Carbon::parse($last_backup_time)->addMonths(1)->addMinutes(-2)->lt(Carbon::now())) {
                    $need_backup = true;
                }
            }
    
            if (!$need_backup) {
                Log::channel('backup')->info('还未到备份时间', ['now' => Carbon::now()->toDateTimeString(), 'last_backup_time' => $last_backup_time, 'backup_interval' => $last_backup_time]);
                return;
            }
        }
    
        Log::channel('backup')->info('数据备份开始:' . $force);
    
        $databaseBackupLog =  DatabaseBackupLog::create([
            'type' => $force ? 'manual' : 'auto',
            'status' => 'running',
            'started_at' => Carbon::now()->toDateTimeString()
        ]);
    
        $exitCode = Artisan::call('backup:run', [
            '--only-db' => true,
        ]);
    
        $output = Artisan::output();
    
        if ($exitCode != 0) {
            $databaseBackupLog->update([
                'status' => 'failure'
            ]);
    
            Log::channel('backup')->error('数据备份失败:', [$output]);
            return;
        }
    
        $databaseBackupLog->update([
            'status' => 'success',
            'finished_at' => Carbon::now()->toDateTimeString()
        ]);
    
        $backupSetting->update([
            'last_backup_time' => $databaseBackupLog->started_at
        ]);
    
        Log::channel('backup')->info('数据备份完成', [$output]);
    }
    }

    然后可以去完善定时任务相关的逻辑。

阅读全文 >>

7月 23

jenkins 太占 cpu,内存了,小服务器难搞。准备试试 Woodpecker CI。

官网: https://woodpecker-ci.org/docs/administration/installation/docker-compose

安装

  1. 创建 woodpecker 目录,创建 docker-compose.yaml 文件。
mkdir woodpecker

cd woodpecker

vim docker-compose.yaml
  1. 填入 docker-compose.yaml 内容。
services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v3
    volumes:
      - ./data/woodpecker-server-data:/var/lib/woodpecker/
    environment:
      - WOODPECKER_OPEN=true
      - WOODPECKER_HOST=${WOODPECKER_HOST}
      - WOODPECKER_GITHUB=true
      - WOODPECKER_GITHUB_CLIENT=${WOODPECKER_GITHUB_CLIENT}
      - WOODPECKER_GITHUB_SECRET=${WOODPECKER_GITHUB_SECRET}
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
    networks:
      - xrnet

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3
    command: agent
    restart: always
    depends_on:
      - woodpecker-server
    volumes:
      - ./data/woodpecker-agent-config:/etc/woodpecker
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
    networks:
      - xrnet

networks:
  xrnet:
    external: true
  1. 配置 .env 文件
vim .env

# 配置
WOODPECKER_GITHUB_CLIENT=
WOODPECKER_GITHUB_SECRET=
WOODPECKER_AGENT_SECRET=

阅读全文 >>

7月 21

Jenkins 是一个开源的自动化服务器,它支持软件开发的持续集成和持续部署(CI/CD)。它允许开发团队自动化构建、测试和部署应用程序,从而提高软件开发的效率和质量。

下边来使用 docker 安装 jenkins,并配置 nginx 代理,进行子路径部署。就是 jenkins 部署到非根目录下,jenkins 只是主域名访问的一部分。

jenkins 官网下载地址:
https://www.jenkins.io/download/

安装

  1. 先创建部署目录和存储数据目录,设置权限。
mkdir jenkins

cd jenkins

mkdir jenkins_home

chown 1000:1000 jenkins_home
  1. 创建 docker-compose.yaml 文件。
services:
  jenkins:
    image: jenkins/jenkins:2.568.1
    container_name: jenkins
    restart: always
    volumes:
      - ./jenkins_home:/var/jenkins_home
      # 挂载宿主机 docker.sock,让 Jenkins 能调用宿主机的 docker
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
      # 挂载宿主机的 SSH 密钥(git clone Codeberg 用)
      - /root/.ssh:/var/jenkins_home/.ssh:ro
    environment:
      - TZ:Asia/Shanghai
    command: >
      --prefix=/cicd
    networks:
      - xrnet

networks:
  xrnet:
    external: true

这里的网络必须和宿主机中 nginx 的网络相同。因为要部署到子路径下,所以要配置 prefix。

  1. 运行起来。
docker compose up -d
  1. 修改已存在的 nginx 配置,添加代理,就使用 cicd 作为子路径代理。新增代理配置。
    location /cicd/ {
        proxy_pass http://jenkins:8080;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host:$server_port;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Context /cicd;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_connect_timeout 60s;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }

    location = /cicd {
        return 301 /cicd/;
    }
  1. 重启 nginx。
docker restart xr-nginx

运行并设置用户名和密码

  1. 打开网站: https://xxx.xxx.com/cicd
  2. 查找密码,输入密码,继续。
    # 查看密码
    cat ./jenkins home/secrets/initialAdminPassword

  3. 安装插件,可以选择安装默认插件。安装过程到最后,如果有失败的可以继续安装,也可以跳过进入到下一步。
  4. 配置第一个管理员用户,这个比较重要,要记住账号和密码。
  5. 继续直到最后。
  6. 安装完成,进入 jenkins 首页。

开始使用

docker compose down

从安装到卸载,jenkins 这玩意,我这破服务器玩不好。

阅读全文 >>

7月 20

前端圈7月大地震:TS 7发布、Bun 11天从Zig迁Rust、Kimi K3登顶、大厂裁员50%:
https://juejin.cn/post/7663095491467411462

https://juejin.cn/post/7663889052081389604

Vue 3.6 发布、Vue 4.0 启动、Vite 8 已落地:
https://juejin.cn/post/7662256328783200319

阿里要求全员卸载 Claude Code:事件始末与深层逻辑:
https://juejin.cn/post/7663822459978137646

阅读全文 >>