webadmin端增加版本号和部署功能

This commit is contained in:
2026-07-12 13:52:22 +08:00
parent 63af3321dd
commit d271ac5b13
16 changed files with 329 additions and 5 deletions
+4
View File
@@ -88,3 +88,7 @@ SHIP_FROM_ADDRESS=河南省郑州市金水区
SHIP_FROM_ADDRESS_DETAIL=杜康酒业仓
SHIP_FROM_LNG=113.665
SHIP_FROM_LAT=34.757
# Admin 概览「发布更新」:调用本机 deploy webhook(与 deploy/auto-release.env 中 SECRET 一致)
# DEPLOY_WEBHOOK_URL=http://127.0.0.1:8095/deploy
# DEPLOY_WEBHOOK_SECRET=change-me-to-a-long-random-string
+13
View File
@@ -263,6 +263,19 @@ enum BenefitLedgerType {
// ─── COMMON ───────────────────────────────────────────
model SystemVersion {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
gitTag String? @map("git_tag") @db.VarChar(128)
commitId String @map("commit_id") @db.VarChar(64)
commitMessage String @map("commit_message") @db.VarChar(512)
branch String? @db.VarChar(64)
deployedBy String? @map("deployed_by") @db.VarChar(32)
deployedAt DateTime @default(now()) @map("deployed_at") @db.DateTime(3)
@@index([deployedAt])
@@map("system_version")
}
model CommonWxAppConfig {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
clientApp ClientApp @unique @map("client_app")
@@ -0,0 +1,68 @@
/**
* 发版成功后写入 system_version。
* 须经 with-api-env.cjs 加载 DATABASE_URL;失败不阻断发版(exit 0)。
*/
const { execSync } = require('child_process');
const { resolve } = require('path');
const apiRoot = resolve(__dirname, '..');
const appRoot = process.env.APP_ROOT || resolve(apiRoot, '../..');
function git(cmd) {
try {
return execSync(cmd, { cwd: appRoot, encoding: 'utf8' }).trim();
} catch {
return null;
}
}
async function main() {
const commitId = git('git rev-parse HEAD');
if (!commitId) {
console.warn('[record-system-version] WARN: 无法读取 git HEAD,跳过');
return;
}
let commitMessage = git('git log -1 --pretty=%s') || '';
if (commitMessage.length > 512) {
commitMessage = commitMessage.slice(0, 512);
}
const gitTag = git('git describe --tags --exact-match') || null;
const branchRaw = git('git rev-parse --abbrev-ref HEAD');
const branch = branchRaw && branchRaw !== 'HEAD' ? branchRaw : null;
const trigger = (process.env.DEPLOY_TRIGGER || '').trim();
let deployedBy = 'manual';
if (trigger) {
if (trigger === 'manual' || trigger === 'admin') {
deployedBy = trigger;
} else {
deployedBy = 'webhook';
}
}
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
try {
const row = await prisma.systemVersion.create({
data: {
gitTag,
commitId,
commitMessage: commitMessage || '(no message)',
branch,
deployedBy,
},
});
console.log(
`[record-system-version] ok id=${row.id} commit=${commitId.slice(0, 7)} tag=${gitTag || '-'} by=${deployedBy}`,
);
} finally {
await prisma.$disconnect();
}
}
main().catch((err) => {
console.warn('[record-system-version] WARN:', err instanceof Error ? err.message : err);
process.exit(0);
});
@@ -52,6 +52,7 @@ export const HqOperationAction = {
PROMO_CODE_UPDATE_STATUS: 'PROMO_CODE_UPDATE_STATUS',
REDEEM_PENDING_COMPLETE: 'REDEEM_PENDING_COMPLETE',
REDEEM_PENDING_REJECT: 'REDEEM_PENDING_REJECT',
DEPLOY_TRIGGER: 'DEPLOY_TRIGGER',
} as const;
export type HqOperationActionCode = (typeof HqOperationAction)[keyof typeof HqOperationAction];
@@ -109,6 +110,7 @@ export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = {
[HqOperationAction.PROMO_CODE_UPDATE_STATUS]: '推广码启停',
[HqOperationAction.REDEEM_PENDING_COMPLETE]: '弱网待处理单-补核销',
[HqOperationAction.REDEEM_PENDING_REJECT]: '弱网待处理单-驳回',
[HqOperationAction.DEPLOY_TRIGGER]: '触发系统发布',
STORE_PAYOUT: '门店打款确认',
};
@@ -11,4 +11,9 @@ export class AdminDashboardController {
stats() {
return this.dashboardService.getStats();
}
@Get('version')
version() {
return this.dashboardService.getLatestVersion();
}
}
@@ -65,4 +65,20 @@ export class AdminDashboardService {
})),
};
}
async getLatestVersion() {
const row = await this.prisma.systemVersion.findFirst({
orderBy: { deployedAt: 'desc' },
});
if (!row) return null;
return {
id: row.id.toString(),
gitTag: row.gitTag,
commitId: row.commitId,
commitMessage: row.commitMessage,
branch: row.branch,
deployedBy: row.deployedBy,
deployedAt: row.deployedAt.toISOString(),
};
}
}
@@ -0,0 +1,22 @@
import { Controller, Post, UseGuards } from '@nestjs/common';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
import { AdminDeployService } from './admin-deploy.service';
@Controller('admin/deploy')
@UseGuards(HqAuthGuard, SuperAdminGuard)
export class AdminDeployController {
constructor(private readonly deployService: AdminDeployService) {}
@Post('trigger')
@HqOperation({
action: HqOperationAction.DEPLOY_TRIGGER,
refType: 'DEPLOY',
batch: true,
})
trigger() {
return this.deployService.triggerDeploy();
}
}
@@ -0,0 +1,49 @@
import { BadRequestException, Injectable, ServiceUnavailableException } from '@nestjs/common';
@Injectable()
export class AdminDeployService {
async triggerDeploy() {
const url = (process.env.DEPLOY_WEBHOOK_URL || 'http://127.0.0.1:8095/deploy').trim();
const secret = (process.env.DEPLOY_WEBHOOK_SECRET || '').trim();
if (!secret) {
throw new ServiceUnavailableException('未配置 DEPLOY_WEBHOOK_SECRET,无法触发发布');
}
let res: Response;
try {
res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Deploy-Token': secret,
},
body: JSON.stringify({ source: 'admin' }),
});
} catch (err) {
throw new ServiceUnavailableException(
`无法连接部署 webhook${err instanceof Error ? err.message : 'network error'}`,
);
}
const text = await res.text();
let data: { ok?: boolean; accepted?: boolean; started?: boolean; message?: string; skipped?: boolean } = {};
try {
data = text ? (JSON.parse(text) as typeof data) : {};
} catch {
throw new ServiceUnavailableException(`部署 webhook 返回非 JSONHTTP ${res.status}`);
}
if (res.status === 403) {
throw new BadRequestException(data.message || '部署 webhook 鉴权失败');
}
if (!res.ok && res.status !== 202) {
throw new ServiceUnavailableException(data.message || `部署 webhook 失败(HTTP ${res.status}`);
}
return {
accepted: true,
started: data.started !== false && !data.skipped,
message: data.message || (data.started === false ? 'deploy debounced' : 'deploy started'),
};
}
}
@@ -49,11 +49,14 @@ import { AdminWechatBindingsController } from './admin-wechat-bindings.controlle
import { AdminWechatBindingsService } from './admin-wechat-bindings.service';
import { AdminHqPermissionsController } from './admin-hq-permissions.controller';
import { AdminHqPermissionsService } from './admin-hq-permissions.service';
import { AdminDeployController } from './admin-deploy.controller';
import { AdminDeployService } from './admin-deploy.service';
@Module({
imports: [CityScopeModule, IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule],
controllers: [
AdminDashboardController,
AdminDeployController,
AdminUsersController,
AdminOrdersController,
AdminStoresController,
@@ -104,6 +107,7 @@ import { AdminHqPermissionsService } from './admin-hq-permissions.service';
AdminRedeemDebugService,
AdminWechatBindingsService,
AdminHqPermissionsService,
AdminDeployService,
SuperAdminGuard,
],
exports: [CityScopeModule],