feat(workflow,trigger): async dispatch + GC schedulers + trigger list polish
@ -2,8 +2,11 @@ package vip.mate.trigger.ingest;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import vip.mate.trigger.dispatch.DispatchResult;
|
||||
import vip.mate.trigger.dispatch.TriggerDispatcher;
|
||||
@ -59,6 +62,26 @@ public class TriggerEventIngestService {
|
||||
private final TriggerPatternMatcher patternMatcher;
|
||||
private final TriggerRateLimiter rateLimiter = new TriggerRateLimiter();
|
||||
|
||||
/** When true (production default), {@code dispatcher.dispatch} runs on
|
||||
* a worker thread so the caller (webhook / scheduler / runner) returns
|
||||
* quickly. When false, ingest runs the workflow inline on the caller
|
||||
* thread; tests pin to false so they can assert against downstream
|
||||
* workflow state immediately after {@code ingest()} returns. */
|
||||
@Value("${mateclaw.workflow.trigger.async-dispatch:true}")
|
||||
private boolean asyncDispatch;
|
||||
|
||||
@Value("${mateclaw.workflow.trigger.dispatch-pool-size:8}")
|
||||
private int dispatchPoolSize;
|
||||
|
||||
@Value("${mateclaw.workflow.trigger.dispatch-queue-capacity:256}")
|
||||
private int dispatchQueueCapacity;
|
||||
|
||||
/** Lazy-built bounded thread pool used when {@link #asyncDispatch} is
|
||||
* true. CallerRunsPolicy is the back-pressure: when the queue is full
|
||||
* the calling thread runs the dispatch itself, which guarantees no
|
||||
* silent drop while still capping in-flight work. */
|
||||
private volatile java.util.concurrent.ThreadPoolExecutor dispatchExecutor;
|
||||
|
||||
public TriggerEventIngestService(TriggerMapper triggerMapper,
|
||||
TriggerEventMapper eventMapper,
|
||||
TriggerDispatcher dispatcher,
|
||||
@ -73,6 +96,44 @@ public class TriggerEventIngestService {
|
||||
this.patternMatcher = patternMatcher;
|
||||
}
|
||||
|
||||
private java.util.concurrent.ThreadPoolExecutor dispatchExecutor() {
|
||||
java.util.concurrent.ThreadPoolExecutor local = dispatchExecutor;
|
||||
if (local != null) return local;
|
||||
synchronized (this) {
|
||||
if (dispatchExecutor == null) {
|
||||
int size = Math.max(1, dispatchPoolSize);
|
||||
int cap = Math.max(1, dispatchQueueCapacity);
|
||||
dispatchExecutor = new java.util.concurrent.ThreadPoolExecutor(
|
||||
size, size,
|
||||
60L, java.util.concurrent.TimeUnit.SECONDS,
|
||||
new java.util.concurrent.LinkedBlockingQueue<>(cap),
|
||||
r -> {
|
||||
Thread t = new Thread(r, "trigger-dispatch-" + System.currentTimeMillis());
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
},
|
||||
new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
|
||||
}
|
||||
return dispatchExecutor;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdownDispatchExecutor() {
|
||||
java.util.concurrent.ThreadPoolExecutor local = dispatchExecutor;
|
||||
if (local != null) {
|
||||
local.shutdown();
|
||||
try {
|
||||
if (!local.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) {
|
||||
local.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
local.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process one envelope through the pipeline. Returns a result per
|
||||
* candidate trigger so callers can surface a partial-accept summary.
|
||||
@ -119,19 +180,29 @@ public class TriggerEventIngestService {
|
||||
if (!rateLimiter.tryAcquire(trigger.getId(), limit, Instant.now())) {
|
||||
return IngestResult.dropped(trigger.getId(), Reason.RATE_LIMITED);
|
||||
}
|
||||
DispatchResult outcome;
|
||||
try {
|
||||
outcome = dispatcher.dispatch(trigger, envelope.data());
|
||||
} catch (Exception e) {
|
||||
// Belt-and-suspenders — the dispatcher already wraps its own
|
||||
// exceptions, but if anything escapes we mark it as DISPATCH_ERROR
|
||||
// and persist last_error so the UI surfaces *why*.
|
||||
log.error("Trigger {} dispatch threw on event ingest: {}",
|
||||
trigger.getId(), e.getMessage(), e);
|
||||
persistDispatchOutcome(trigger, DispatchResult.failed("dispatch threw: " + e.getMessage()));
|
||||
return IngestResult.dropped(trigger.getId(), Reason.DISPATCH_ERROR);
|
||||
if (asyncDispatch) {
|
||||
// Async path — submit dispatch to the bounded pool so the
|
||||
// caller (webhook / scheduler / runner thread) returns
|
||||
// quickly. Bookkeeping happens inside the worker, so
|
||||
// last_error / fireCount stay accurate. The IngestResult
|
||||
// signals "accepted, fanning out" rather than "ran to
|
||||
// completion"; that's the honest contract for an async
|
||||
// pipeline. CallerRunsPolicy on the executor means we
|
||||
// self-throttle instead of dropping under back-pressure.
|
||||
try {
|
||||
dispatchExecutor().execute(() -> runDispatchAndPersist(trigger, envelope));
|
||||
} catch (Exception e) {
|
||||
log.error("Trigger {} dispatch submit failed: {}",
|
||||
trigger.getId(), e.getMessage(), e);
|
||||
persistDispatchOutcome(trigger,
|
||||
DispatchResult.failed("dispatch submit failed: " + e.getMessage()));
|
||||
return IngestResult.dropped(trigger.getId(), Reason.DISPATCH_ERROR);
|
||||
}
|
||||
return IngestResult.fired(trigger.getId());
|
||||
}
|
||||
persistDispatchOutcome(trigger, outcome);
|
||||
// Synchronous path — used by tests and any deployment that
|
||||
// explicitly opts out via mateclaw.workflow.trigger.async-dispatch=false.
|
||||
DispatchResult outcome = runDispatchAndPersist(trigger, envelope);
|
||||
return switch (outcome.kind()) {
|
||||
case FIRED -> IngestResult.fired(trigger.getId());
|
||||
case SKIPPED -> IngestResult.dropped(trigger.getId(), Reason.DISPATCH_SKIPPED);
|
||||
@ -139,6 +210,22 @@ public class TriggerEventIngestService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Runs dispatch + bookkeeping on whatever thread invokes it (the
|
||||
* caller in sync mode, a worker in async mode). Returns the
|
||||
* outcome so sync callers can map it back to an IngestResult. */
|
||||
private DispatchResult runDispatchAndPersist(TriggerEntity trigger, TriggerEventEnvelope envelope) {
|
||||
DispatchResult outcome;
|
||||
try {
|
||||
outcome = dispatcher.dispatch(trigger, envelope.data());
|
||||
} catch (Exception e) {
|
||||
log.error("Trigger {} dispatch threw on event ingest: {}",
|
||||
trigger.getId(), e.getMessage(), e);
|
||||
outcome = DispatchResult.failed("dispatch threw: " + e.getMessage());
|
||||
}
|
||||
persistDispatchOutcome(trigger, outcome);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the trigger row's bookkeeping based on the dispatch outcome.
|
||||
* Only FIRED bumps {@code fireCount} and {@code lastFiredAt} — SKIPPED
|
||||
@ -210,6 +297,28 @@ public class TriggerEventIngestService {
|
||||
LocalDateTime.ofInstant(Instant.now(), ZoneOffset.systemDefault())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic sweep of expired {@code mate_trigger_event} dedup rows.
|
||||
* Default cadence is every 5 minutes, tunable via
|
||||
* {@code mateclaw.workflow.trigger.dedup-sweep-interval-ms}. The
|
||||
* initial delay matches the cadence so a JVM that just started doesn't
|
||||
* race {@code recordDedupRow} for the same window.
|
||||
*/
|
||||
@Scheduled(
|
||||
fixedDelayString = "${mateclaw.workflow.trigger.dedup-sweep-interval-ms:300000}",
|
||||
initialDelayString = "${mateclaw.workflow.trigger.dedup-sweep-initial-delay-ms:300000}")
|
||||
public void scheduledSweepExpired() {
|
||||
try {
|
||||
int dropped = sweepExpired();
|
||||
if (dropped > 0) {
|
||||
log.info("[TriggerIngest] swept {} expired dedup rows", dropped);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Best-effort — never let the sweep crash the scheduler thread.
|
||||
log.warn("[TriggerIngest] dedup sweep failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
PATTERN_MISMATCH, BOT_SELF, DUPLICATE, RATE_LIMITED, EXHAUSTED,
|
||||
/** Dispatcher returned SKIPPED — pre-flight rejected (no published revision, etc.). */
|
||||
|
||||
@ -2,7 +2,9 @@ package vip.mate.workflow.runtime;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import vip.mate.workflow.model.WorkflowPayloadEntity;
|
||||
import vip.mate.workflow.repository.WorkflowPayloadMapper;
|
||||
@ -15,6 +17,7 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -38,6 +41,7 @@ import java.util.UUID;
|
||||
* v1. The fs tier is what unblocks local dev / docker / private deploys
|
||||
* that don't have an object store configured.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PayloadStore {
|
||||
|
||||
@ -50,17 +54,20 @@ public class PayloadStore {
|
||||
private final long inlineMaxBytes;
|
||||
private final long hardCapBytes;
|
||||
private final Path fsRoot;
|
||||
private final long retentionDays;
|
||||
|
||||
public PayloadStore(WorkflowPayloadMapper payloadMapper,
|
||||
ObjectMapper objectMapper,
|
||||
@Value("${mateclaw.workflow.payload.inline-max-bytes:262144}") long inlineMaxBytes,
|
||||
@Value("${mateclaw.workflow.payload.hard-cap-bytes:52428800}") long hardCapBytes,
|
||||
@Value("${mateclaw.workflow.payload.fs.root:./data/workflow-payload}") String fsRoot) {
|
||||
@Value("${mateclaw.workflow.payload.fs.root:./data/workflow-payload}") String fsRoot,
|
||||
@Value("${mateclaw.workflow.payload.retention-days:30}") long retentionDays) {
|
||||
this.payloadMapper = payloadMapper;
|
||||
this.objectMapper = objectMapper;
|
||||
this.inlineMaxBytes = inlineMaxBytes;
|
||||
this.hardCapBytes = hardCapBytes;
|
||||
this.fsRoot = Path.of(fsRoot).toAbsolutePath();
|
||||
this.retentionDays = retentionDays;
|
||||
}
|
||||
|
||||
/** Store a UTF-8 string payload and return its stable URI. */
|
||||
@ -172,6 +179,71 @@ public class PayloadStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop payload rows older than {@code retention-days}. Tombstones the
|
||||
* filesystem files for fs-tier payloads in the same pass so the disk
|
||||
* doesn't keep growing once the DB row is gone. Returns the number of
|
||||
* rows actually deleted; primarily for tests + log lines.
|
||||
*
|
||||
* <p>v0 deletes by absolute age rather than walking the
|
||||
* {@code mate_workflow_run} graph — runs that finish stay queryable
|
||||
* for {@code retention-days} from the payload-write timestamp, which
|
||||
* is "good enough" for an alpha. v1 can switch to run-state-driven
|
||||
* GC ({@code state IN ('succeeded','failed') AND completed_at <
|
||||
* threshold}) once the operator UI exposes a "preserve forever" flag
|
||||
* for runs the customer wants kept.
|
||||
*/
|
||||
public int sweepExpired() {
|
||||
if (retentionDays <= 0) return 0;
|
||||
LocalDateTime cutoff = LocalDateTime.now().minusDays(retentionDays);
|
||||
List<WorkflowPayloadEntity> stale = payloadMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<WorkflowPayloadEntity>()
|
||||
.lt(WorkflowPayloadEntity::getCreatedAt, cutoff));
|
||||
if (stale.isEmpty()) return 0;
|
||||
int deleted = 0;
|
||||
for (WorkflowPayloadEntity row : stale) {
|
||||
// Best-effort fs cleanup before the row goes — the row IS the
|
||||
// foreign key the file is reachable through; if the row goes
|
||||
// first the file becomes orphaned.
|
||||
if (STORAGE_KIND_FS.equals(row.getStorageKind()) && row.getStorageRef() != null) {
|
||||
try {
|
||||
Files.deleteIfExists(fsRoot.resolve(row.getStorageRef()));
|
||||
} catch (IOException e) {
|
||||
log.warn("[PayloadStore] fs delete failed for {}: {}",
|
||||
row.getStorageRef(), e.getMessage());
|
||||
}
|
||||
}
|
||||
try {
|
||||
payloadMapper.deleteById(row.getId());
|
||||
deleted++;
|
||||
} catch (Exception e) {
|
||||
log.warn("[PayloadStore] db delete failed for payload {}: {}",
|
||||
row.getPayloadUri(), e.getMessage());
|
||||
}
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic sweep — runs once an hour by default. Tunable via
|
||||
* {@code mateclaw.workflow.payload.sweep-interval-ms}. Skips a tick
|
||||
* silently when retentionDays = 0 (operator opted out of GC).
|
||||
*/
|
||||
@Scheduled(
|
||||
fixedDelayString = "${mateclaw.workflow.payload.sweep-interval-ms:3600000}",
|
||||
initialDelayString = "${mateclaw.workflow.payload.sweep-initial-delay-ms:600000}")
|
||||
public void scheduledSweepExpired() {
|
||||
try {
|
||||
int dropped = sweepExpired();
|
||||
if (dropped > 0) {
|
||||
log.info("[PayloadStore] swept {} expired payload rows (retention={} days)",
|
||||
dropped, retentionDays);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[PayloadStore] periodic sweep failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Wrapper exception for payload-store failures. */
|
||||
public static class PayloadStoreException extends RuntimeException {
|
||||
public PayloadStoreException(String message) { super(message); }
|
||||
|
||||
@ -911,6 +911,13 @@ export interface TriggerSummary {
|
||||
fireCount: number
|
||||
maxFires: number
|
||||
lastFiredAt?: string
|
||||
/** Stamp of the last dispatch attempt regardless of outcome (FIRED /
|
||||
* SKIPPED / FAILED). Distinguishes "never attempted" from
|
||||
* "attempted but the pre-flight skipped". */
|
||||
lastDispatchedAt?: string
|
||||
/** Most recent dispatch outcome message; null when the last attempt
|
||||
* fired cleanly. */
|
||||
lastError?: string
|
||||
patternVersion: number
|
||||
createTime: string
|
||||
updateTime: string
|
||||
|
||||
@ -2077,6 +2077,15 @@ export default {
|
||||
},
|
||||
targetWorkflowSelect: 'Pick a published workflow',
|
||||
targetWorkflowEmpty: 'No published workflows yet.',
|
||||
lastDispatchedAt: 'Last attempt',
|
||||
patternTypeLabels: {
|
||||
cron: 'Cron schedule',
|
||||
channel_message: 'Channel message',
|
||||
content_match: 'Content match',
|
||||
agent_lifecycle: 'Agent lifecycle',
|
||||
workflow_completion: 'Workflow completion',
|
||||
webhook: 'Webhook',
|
||||
},
|
||||
patternHints: {
|
||||
cron: 'Example: {"cron":"0 0 * * * *","timezone":"UTC"} — every cron change bumps pattern_version.',
|
||||
channel_message: 'Optional channelType (e.g. feishu) and senderEquals narrow the match.',
|
||||
|
||||
@ -2089,6 +2089,15 @@ export default {
|
||||
},
|
||||
targetWorkflowSelect: '选择已发布的工作流',
|
||||
targetWorkflowEmpty: '尚无已发布的工作流。',
|
||||
lastDispatchedAt: '最近一次尝试',
|
||||
patternTypeLabels: {
|
||||
cron: '定时任务',
|
||||
channel_message: '渠道消息',
|
||||
content_match: '内容匹配',
|
||||
agent_lifecycle: '智能体生命周期',
|
||||
workflow_completion: '工作流完成',
|
||||
webhook: 'Webhook',
|
||||
},
|
||||
patternHints: {
|
||||
cron: '示例:{"cron":"0 0 * * * *","timezone":"Asia/Shanghai"} —— 每次修改 cron 都会让 pattern_version 自增。',
|
||||
channel_message: '可填 channelType(如 feishu)和 senderEquals 进一步过滤。',
|
||||
|
||||
@ -27,16 +27,28 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in triggers" :key="row.id">
|
||||
<td>{{ row.name || t('triggers.unnamed') }}</td>
|
||||
<td>
|
||||
<code>{{ row.patternType }}</code>
|
||||
<div class="pattern-detail">{{ row.patternJson }}</div>
|
||||
<div class="trigger-name">{{ row.name || t('triggers.unnamed') }}</div>
|
||||
<div v-if="row.lastError" class="trigger-last-error" :title="row.lastError">
|
||||
⚠ {{ truncateError(row.lastError) }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="trigger-type-pill" :title="row.patternType">{{ patternTypeLabel(row.patternType) }}</span>
|
||||
<div class="pattern-summary" :title="row.patternJson">{{ patternSummary(row) }}</div>
|
||||
</td>
|
||||
<td>{{ formatTarget(row) }}</td>
|
||||
<td>{{ t('triggers.rateUnit', { count: row.rateLimitPerMin }) }}</td>
|
||||
<td>{{ row.fireCount }}<span v-if="row.maxFires > 0"> / {{ row.maxFires }}</span></td>
|
||||
<td>{{ row.patternVersion }}</td>
|
||||
<td>{{ formatTime(row.lastFiredAt) }}</td>
|
||||
<td>
|
||||
<div>{{ formatTime(row.lastFiredAt) }}</div>
|
||||
<div v-if="row.lastDispatchedAt && row.lastDispatchedAt !== row.lastFiredAt"
|
||||
class="trigger-attempt-time"
|
||||
:title="t('triggers.lastDispatchedAt')">
|
||||
{{ formatTime(row.lastDispatchedAt) }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" :checked="row.enabled" @change="toggleEnabled(row)" />
|
||||
@ -228,6 +240,58 @@ function formatTime(iso?: string) {
|
||||
return iso.replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
function patternTypeLabel(pt: string): string {
|
||||
const key = `triggers.patternTypeLabels.${pt}`
|
||||
const localized = t(key, '')
|
||||
return localized && localized !== key ? localized : pt
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a one-line, human-readable summary of the pattern_json so the
|
||||
* list reads as a product surface rather than a developer console. The
|
||||
* raw JSON is still available via the row's title attribute (tooltip)
|
||||
* and the edit form's "raw pattern_json (advanced)" details element.
|
||||
*/
|
||||
function patternSummary(row: TriggerSummary): string {
|
||||
if (!row.patternJson) return '-'
|
||||
let parsed: Record<string, unknown> = {}
|
||||
try { parsed = JSON.parse(row.patternJson) } catch { return row.patternJson }
|
||||
switch (row.patternType) {
|
||||
case 'cron': {
|
||||
const cron = parsed.cron ?? ''
|
||||
const tz = parsed.timezone ?? ''
|
||||
return tz ? `${cron} (${tz})` : `${cron}`
|
||||
}
|
||||
case 'channel_message': {
|
||||
const ch = parsed.channelType ? `${parsed.channelType}` : t('triggers.pattern.channelTypeAny')
|
||||
const sender = parsed.senderEquals ? ` · @${parsed.senderEquals}` : ''
|
||||
return `${ch}${sender}`
|
||||
}
|
||||
case 'content_match': {
|
||||
return parsed.substring ? `"${parsed.substring}"` : '—'
|
||||
}
|
||||
case 'agent_lifecycle': {
|
||||
const aid = parsed.agentId ? `#${parsed.agentId}` : t('triggers.pattern.agentIdPlaceholder')
|
||||
const ph = parsed.phase ? ` · ${parsed.phase}` : ''
|
||||
return `${aid}${ph}`
|
||||
}
|
||||
case 'workflow_completion': {
|
||||
const src = parsed.sourceWorkflowId ? `#${parsed.sourceWorkflowId}` : t('triggers.pattern.sourceWorkflowAny')
|
||||
const st = parsed.stateFilter ? ` · ${parsed.stateFilter}` : ''
|
||||
return `${src}${st}`
|
||||
}
|
||||
case 'webhook':
|
||||
return t('triggers.pattern.webhookHeader')
|
||||
default:
|
||||
return row.patternJson
|
||||
}
|
||||
}
|
||||
|
||||
function truncateError(msg: string | undefined): string {
|
||||
if (!msg) return ''
|
||||
return msg.length <= 64 ? msg : msg.slice(0, 60) + '…'
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null
|
||||
formState.value = emptyForm()
|
||||
@ -337,6 +401,46 @@ watch(workspaceId, reload)
|
||||
opacity: 0.7;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.pattern-summary {
|
||||
font-family: 'JetBrains Mono', Consolas, monospace;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
margin-top: 3px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 240px;
|
||||
}
|
||||
.trigger-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
.trigger-last-error {
|
||||
font-size: 11px;
|
||||
color: var(--mc-danger, #c0392b);
|
||||
margin-top: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 240px;
|
||||
cursor: help;
|
||||
}
|
||||
.trigger-attempt-time {
|
||||
font-size: 10px;
|
||||
opacity: 0.55;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.trigger-type-pill {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: 999px;
|
||||
background: var(--mc-bg-muted, rgba(0, 0, 0, 0.06));
|
||||
color: var(--mc-text-secondary, inherit);
|
||||
text-transform: lowercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
:lang(zh-CN) .trigger-type-pill { text-transform: none; }
|
||||
.empty-row {
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
|
||||
367
outputs/北京公交票务评估/build_design_doc.py
Normal file
@ -0,0 +1,367 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.section import WD_SECTION
|
||||
from docx.enum.table import WD_ALIGN_VERTICAL, WD_TABLE_ALIGNMENT
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Inches, Pt, RGBColor
|
||||
|
||||
|
||||
OUT = Path("/Users/mate/Codes/mate/mateclaw/outputs/北京公交票务评估/北京公交集团票务综合管理平台系统升级-详细设计与Codex实施方案.docx")
|
||||
|
||||
BLUE = RGBColor(46, 116, 181)
|
||||
DARK_BLUE = RGBColor(31, 77, 120)
|
||||
INK = RGBColor(11, 37, 69)
|
||||
MUTED = RGBColor(96, 108, 122)
|
||||
LIGHT = "F2F4F7"
|
||||
LIGHT_BLUE = "E8EEF5"
|
||||
CALLOUT = "F4F6F9"
|
||||
|
||||
|
||||
def set_cell_shading(cell, fill: str) -> None:
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
shd = tc_pr.find(qn("w:shd"))
|
||||
if shd is None:
|
||||
shd = OxmlElement("w:shd")
|
||||
tc_pr.append(shd)
|
||||
shd.set(qn("w:fill"), fill)
|
||||
|
||||
|
||||
def set_cell_margins(cell, top=80, start=120, bottom=80, end=120) -> None:
|
||||
tc = cell._tc
|
||||
tc_pr = tc.get_or_add_tcPr()
|
||||
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
||||
if tc_mar is None:
|
||||
tc_mar = OxmlElement("w:tcMar")
|
||||
tc_pr.append(tc_mar)
|
||||
for m, v in [("top", top), ("start", start), ("bottom", bottom), ("end", end)]:
|
||||
node = tc_mar.find(qn(f"w:{m}"))
|
||||
if node is None:
|
||||
node = OxmlElement(f"w:{m}")
|
||||
tc_mar.append(node)
|
||||
node.set(qn("w:w"), str(v))
|
||||
node.set(qn("w:type"), "dxa")
|
||||
|
||||
|
||||
def set_table_width(table, widths: list[float]) -> None:
|
||||
table.autofit = False
|
||||
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
for row in table.rows:
|
||||
for idx, cell in enumerate(row.cells):
|
||||
cell.width = Inches(widths[idx])
|
||||
set_cell_margins(cell)
|
||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
||||
|
||||
|
||||
def set_run_font(run, size=None, bold=None, color=None, name="Calibri"):
|
||||
run.font.name = name
|
||||
run._element.rPr.rFonts.set(qn("w:ascii"), name)
|
||||
run._element.rPr.rFonts.set(qn("w:hAnsi"), name)
|
||||
run._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
|
||||
if size is not None:
|
||||
run.font.size = Pt(size)
|
||||
if bold is not None:
|
||||
run.bold = bold
|
||||
if color is not None:
|
||||
run.font.color.rgb = color
|
||||
|
||||
|
||||
def add_para(doc: Document, text: str = "", style: str | None = None, bold=False, color=None, size=None, align=None):
|
||||
p = doc.add_paragraph(style=style)
|
||||
if align is not None:
|
||||
p.alignment = align
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
p.paragraph_format.line_spacing = 1.1
|
||||
if text:
|
||||
r = p.add_run(text)
|
||||
set_run_font(r, size=size, bold=bold, color=color)
|
||||
return p
|
||||
|
||||
|
||||
def add_bullets(doc: Document, items: Iterable[str]) -> None:
|
||||
for item in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
p.paragraph_format.line_spacing = 1.167
|
||||
r = p.add_run(item)
|
||||
set_run_font(r, size=10.5)
|
||||
|
||||
|
||||
def add_numbered(doc: Document, items: Iterable[str]) -> None:
|
||||
for item in items:
|
||||
p = doc.add_paragraph(style="List Number")
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
p.paragraph_format.line_spacing = 1.167
|
||||
r = p.add_run(item)
|
||||
set_run_font(r, size=10.5)
|
||||
|
||||
|
||||
def add_heading(doc: Document, text: str, level: int):
|
||||
p = doc.add_heading(text, level=level)
|
||||
if level == 1:
|
||||
p.paragraph_format.space_before = Pt(16)
|
||||
p.paragraph_format.space_after = Pt(8)
|
||||
elif level == 2:
|
||||
p.paragraph_format.space_before = Pt(12)
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
else:
|
||||
p.paragraph_format.space_before = Pt(8)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
for r in p.runs:
|
||||
set_run_font(r, size={1: 16, 2: 13, 3: 12}.get(level, 11), bold=True, color=BLUE if level < 3 else DARK_BLUE)
|
||||
return p
|
||||
|
||||
|
||||
def add_table(doc: Document, headers: list[str], rows: list[list[str]], widths: list[float], header_fill=LIGHT) -> None:
|
||||
table = doc.add_table(rows=1, cols=len(headers))
|
||||
table.style = "Table Grid"
|
||||
hdr = table.rows[0].cells
|
||||
for i, h in enumerate(headers):
|
||||
hdr[i].text = h
|
||||
set_cell_shading(hdr[i], header_fill)
|
||||
set_cell_margins(hdr[i])
|
||||
for p in hdr[i].paragraphs:
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
for r in p.runs:
|
||||
set_run_font(r, size=9.5, bold=True, color=INK)
|
||||
for row in rows:
|
||||
cells = table.add_row().cells
|
||||
for i, value in enumerate(row):
|
||||
cells[i].text = value
|
||||
set_cell_margins(cells[i])
|
||||
for p in cells[i].paragraphs:
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
p.paragraph_format.line_spacing = 1.08
|
||||
for r in p.runs:
|
||||
set_run_font(r, size=9)
|
||||
set_table_width(table, widths)
|
||||
doc.add_paragraph()
|
||||
|
||||
|
||||
def add_callout(doc: Document, title: str, body: str) -> None:
|
||||
table = doc.add_table(rows=1, cols=1)
|
||||
table.style = "Table Grid"
|
||||
cell = table.cell(0, 0)
|
||||
set_cell_shading(cell, CALLOUT)
|
||||
set_cell_margins(cell, top=120, bottom=120, start=160, end=160)
|
||||
p = cell.paragraphs[0]
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(title)
|
||||
set_run_font(r, size=10.5, bold=True, color=DARK_BLUE)
|
||||
p2 = cell.add_paragraph()
|
||||
p2.paragraph_format.space_after = Pt(0)
|
||||
r2 = p2.add_run(body)
|
||||
set_run_font(r2, size=10)
|
||||
set_table_width(table, [6.5])
|
||||
doc.add_paragraph()
|
||||
|
||||
|
||||
def configure_document(doc: Document) -> None:
|
||||
section = doc.sections[0]
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1)
|
||||
section.right_margin = Inches(1)
|
||||
section.header_distance = Inches(0.492)
|
||||
section.footer_distance = Inches(0.492)
|
||||
|
||||
styles = doc.styles
|
||||
normal = styles["Normal"]
|
||||
normal.font.name = "Calibri"
|
||||
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
|
||||
normal.font.size = Pt(11)
|
||||
normal.paragraph_format.space_after = Pt(6)
|
||||
normal.paragraph_format.line_spacing = 1.1
|
||||
|
||||
for name, size, color in [("Heading 1", 16, BLUE), ("Heading 2", 13, BLUE), ("Heading 3", 12, DARK_BLUE)]:
|
||||
st = styles[name]
|
||||
st.font.name = "Calibri"
|
||||
st._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
|
||||
st.font.size = Pt(size)
|
||||
st.font.color.rgb = color
|
||||
st.font.bold = True
|
||||
|
||||
header = section.header.paragraphs[0]
|
||||
header.text = "北京公交集团票务综合管理平台系统升级 | 详细设计与 Codex 实施方案"
|
||||
header.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
for r in header.runs:
|
||||
set_run_font(r, size=9, color=MUTED)
|
||||
|
||||
footer = section.footer.paragraphs[0]
|
||||
footer.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||
footer.text = "内部评估文件"
|
||||
for r in footer.runs:
|
||||
set_run_font(r, size=9, color=MUTED)
|
||||
|
||||
|
||||
def build() -> None:
|
||||
doc = Document()
|
||||
configure_document(doc)
|
||||
|
||||
add_para(doc, "技术方案", bold=True, color=MUTED, size=11)
|
||||
title = doc.add_paragraph()
|
||||
title.paragraph_format.space_after = Pt(4)
|
||||
r = title.add_run("北京公交集团票务综合管理平台系统升级")
|
||||
set_run_font(r, size=24, bold=True, color=INK)
|
||||
subtitle = doc.add_paragraph()
|
||||
subtitle.paragraph_format.space_after = Pt(16)
|
||||
r = subtitle.add_run("详细设计与 Codex 协作实施方案")
|
||||
set_run_font(r, size=15, color=MUTED)
|
||||
|
||||
meta = [
|
||||
["文档用途", "用于项目立项、报价拆分、研发实施和 Codex 协作开发规划"],
|
||||
["编制日期", date.today().isoformat()],
|
||||
["需求来源", "《北京公交集团票务综合管理平台系统升级-需求分析说明书(初稿0506)》"],
|
||||
["版本", "V1.0"],
|
||||
]
|
||||
add_table(doc, ["字段", "内容"], meta, [1.3, 5.2], header_fill=LIGHT_BLUE)
|
||||
|
||||
add_callout(
|
||||
doc,
|
||||
"总体判断",
|
||||
"本项目属于存量核心业务系统重构升级,重点不是页面生成,而是票据库存流水、票单结算、报表口径、数据权限、历史数据迁移和国产化适配。Codex 可显著提升需求整理、重复代码生成、测试补齐和文档产出效率,但架构边界、业务规则确认和上线割接仍需人工主导。",
|
||||
)
|
||||
|
||||
add_heading(doc, "1. 项目范围与建设目标", 1)
|
||||
add_para(doc, "本次建设目标是在继承现有生产系统主要能力的基础上,完成业务流程重构、国产化数据库迁移、权限和数据范围治理、报表整合、接口标准化和运维能力提升。系统面向集团、分公司和票务室多级用户,覆盖有人售票、无人售票、票库、票柜、票袋、票单、充值卡、特殊业务和统计报表等票务经营管理场景。")
|
||||
add_bullets(doc, [
|
||||
"完整保留并优化核心票务业务闭环:印制、调拨、核销、配票、结算、退票、库存查询和流水追溯。",
|
||||
"整合同类报表,按集团、分公司、票务室三级数据权限展示统计结果。",
|
||||
"完成 Oracle 历史数据到达梦数据库的迁移、校验和兼容适配。",
|
||||
"预留并分阶段实现与 IC 卡系统、数据湖、二维码系统、票款清分系统、电子签章系统的对接。",
|
||||
"建立可审计、可测试、可持续迭代的工程体系,支持 Codex 辅助开发和自动化校验。",
|
||||
])
|
||||
|
||||
add_heading(doc, "2. 总体技术架构", 1)
|
||||
add_table(doc, ["层级", "建议技术", "设计说明"], [
|
||||
["前端展示层", "Vue 3 + TypeScript + Vite + Element Plus", "承载菜单导航、列表查询、业务表单、报表展示、打印和导入导出入口。"],
|
||||
["后端服务层", "Java 17 + Spring Boot 3 + MyBatis-Plus", "实现业务编排、事务控制、权限校验、库存流水、报表聚合和接口服务。"],
|
||||
["数据存储层", "达梦 DM8 + Redis + OBS/MinIO", "达梦作为主业务库,Redis 用于会话/缓存/短期任务状态,文件对象存储用于模板、导入文件和导出文件。"],
|
||||
["集成层", "REST/OpenAPI + 批量文件导入 + 定时任务", "对外提供标准接口;对无网络银行清分场景保留线下 Excel/CSV 导入;定时同步基础信息和报表数据。"],
|
||||
["运维层", "Nginx + 应用服务 + 日志审计 + 监控告警", "满足国产化部署、日志追溯、性能监控和上线回滚要求。"],
|
||||
], [1.15, 1.75, 3.6], header_fill=LIGHT_BLUE)
|
||||
|
||||
add_heading(doc, "3. 系统模块设计", 1)
|
||||
modules = [
|
||||
["基础信息", "票务室、线路、票价、车辆、司售、普票类型、充值类型、机具、多样化运营类型、IC 卡类型", "以 CRUD 和同步查询为主,需统一数据权限和停用机制。"],
|
||||
["票库管理", "印制、调拨、核销、调账、票库库存、票库出入库查询", "核心在审批状态、库存流水、票号段合法性和跨公司调拨。"],
|
||||
["票柜管理", "票柜库存、入库、出库、配票申请、票务室调票", "承接票库到票务室的库存流转,需要单张/批量调票能力。"],
|
||||
["票单管理", "票袋、票单配票、票单结算、票单查询、票袋调票、重新结算、票号查询", "项目最复杂模块,需要严格状态机和结算口径。"],
|
||||
["无人售与特殊业务", "异物、大额币、残币、无人售线路结算、多样化收入、特殊票", "重点是异常状态、待销毁/领取/入账流程和批量导入。"],
|
||||
["退票管理", "票袋退票、票柜退票", "支持连续退票和单张退票,必须回写库存和流水。"],
|
||||
["充值管理", "IC 卡库存、入库、出库、网点库存、售出、充值收入", "需要处理网点维度库存、售出导入和收入记录。"],
|
||||
["统计报表", "集团、分公司、票务室多级报表、趋势图、Excel 导出", "需先冻结报表口径,避免开发完成后重复返工。"],
|
||||
["系统管理", "用户、角色、权限、菜单、阈值、日志", "为全系统提供认证、授权、数据范围和审计能力。"],
|
||||
]
|
||||
add_table(doc, ["模块", "主要功能", "设计重点"], modules, [1.25, 2.9, 2.35])
|
||||
|
||||
add_heading(doc, "4. 核心业务状态与流水设计", 1)
|
||||
add_para(doc, "票据类业务必须采用“当前库存 + 流水台账 + 状态机”的组合模型。只存库存余额无法满足审计、调账、退票、票号查询和异常追溯要求。")
|
||||
add_table(doc, ["业务对象", "关键状态", "关键校验"], [
|
||||
["印制申请", "待审核、不同意、待确认、完成", "待审核/不同意可修改;完成后生成票库入库流水。"],
|
||||
["调拨申请", "待审核、不同意、待确认、完成", "校验调出库存、调入公司、票号段不重叠。"],
|
||||
["核销申请", "待审核、完成、驳回", "核销后票号段不可再配出,保留原因和审批记录。"],
|
||||
["配票申请", "待处理、已配票、退回、完成", "票柜库存扣减,票袋或票单库存增加。"],
|
||||
["票单结算", "已配票、已结算、重新结算中、已更正", "销售张数和金额由剩余票号段反推,保留旧结算记录。"],
|
||||
["退票", "申请、确认、完成、驳回", "退票票号不能已核销、不能重复退票,完成后回写对应库存。"],
|
||||
], [1.3, 2.1, 3.1])
|
||||
|
||||
add_heading(doc, "5. 数据模型设计原则", 1)
|
||||
add_bullets(doc, [
|
||||
"基础主数据表:组织、分公司、票务室、线路、车辆、司售、票价、票种、充值类型、机具等。",
|
||||
"库存余额表:按库存主体、票种、票价、票组、起止号、状态聚合当前可用数量。",
|
||||
"库存流水表:记录每一次入库、出库、调拨、退票、核销、调账、配票、结算的来源单据和操作人。",
|
||||
"业务单据表:每类申请、审核、确认、结算均独立建单,避免把审批状态混入库存余额。",
|
||||
"报表宽表/汇总表:对日结、月结、趋势报表建立可重算的汇总层,减少实时聚合压力。",
|
||||
"审计表:记录登录、菜单访问、增删改、导入导出、审批、结账等关键操作。",
|
||||
])
|
||||
|
||||
add_heading(doc, "6. 权限与数据范围设计", 1)
|
||||
add_table(doc, ["角色", "功能权限", "数据范围"], [
|
||||
["集团管理员", "最高权限,可维护菜单、角色、用户和全局参数", "全集团数据,可分配集团/分公司/票务室数据权限。"],
|
||||
["集团操作员", "按角色授权使用业务功能", "可查看被授权的分公司和票务室数据。"],
|
||||
["分公司管理员", "维护本分公司及下属票务室用户和权限", "默认本分公司数据,可细分到票务室。"],
|
||||
["分公司操作员", "使用分公司层面业务和报表功能", "本分公司及授权票务室数据。"],
|
||||
["票务室操作员", "操作票柜、票袋、票单、结算等票务室业务", "仅本票务室数据。"],
|
||||
], [1.25, 2.55, 2.7], header_fill=LIGHT_BLUE)
|
||||
add_callout(doc, "权限实现建议", "权限必须拆成菜单权限、按钮权限、数据范围权限和字段/导出权限。所有查询接口必须统一经过数据范围拦截器,避免前端隐藏菜单但后端接口越权。")
|
||||
|
||||
add_heading(doc, "7. 接口与集成设计", 1)
|
||||
add_table(doc, ["系统/对象", "对接方式", "一期处理建议"], [
|
||||
["IC 卡系统", "接口同步或定时任务", "优先同步线路、车辆、司售、IC 卡基础信息;接口不稳定时保留手动导入兜底。"],
|
||||
["数据湖/二维码系统", "标准 REST/API 或数据推送", "先定义统一数据服务接口、鉴权、频率、字段口径;具体联调作为独立里程碑。"],
|
||||
["银行清分清点系统", "线下 Excel/CSV 导入", "按银行无网环境处理,建立模板、校验、导入日志和失败明细下载。"],
|
||||
["票款清分/胆款系统", "批量同步或接口推送", "以票务系统生成的清分日报为主数据源,明确对账字段。"],
|
||||
["电子签章系统", "第三方接口", "建议一期预留签章对象、签章状态和附件字段,实际签章联调单独报价/排期。"],
|
||||
], [1.55, 1.55, 3.4])
|
||||
|
||||
add_heading(doc, "8. 数据迁移方案", 1)
|
||||
add_numbered(doc, [
|
||||
"盘点 Oracle 现有 79 张表、序列、视图、函数、触发器和历史附件,建立字段映射表。",
|
||||
"完成达梦库结构设计,明确字段类型、索引、主键、唯一约束和历史兼容字段。",
|
||||
"开发迁移脚本,先迁移基础主数据,再迁移库存、单据、结算、报表历史数据。",
|
||||
"执行数据清洗:重复票号、缺失组织、异常状态、金额不平、非法日期等问题生成清洗报告。",
|
||||
"进行三轮迁移演练:开发环境、测试环境、准生产环境,每轮输出数量核对和金额核对报告。",
|
||||
"上线割接时冻结旧系统写入,完成增量迁移、业务抽样验证和回滚预案确认。",
|
||||
])
|
||||
|
||||
add_heading(doc, "9. Codex 协作开发流程", 1)
|
||||
add_table(doc, ["阶段", "Codex 可承担工作", "人工控制点"], [
|
||||
["需求结构化", "从 Word/Excel 提取功能项、生成需求池、补充初版验收点", "业务人员确认字段、流程、报表口径和删除/保留范围。"],
|
||||
["设计阶段", "生成表结构草案、接口草案、状态机草案、测试场景清单", "架构师确认边界、事务、权限模型和关键业务规则。"],
|
||||
["编码阶段", "生成 CRUD、Service、Mapper、Controller、前端页面、导入导出、测试用例", "开发负责人审查代码风格、事务完整性和异常处理。"],
|
||||
["测试阶段", "补接口测试、构造 Mock 数据、生成回归脚本、修复构建问题", "测试负责人确认场景覆盖和真实业务数据抽样。"],
|
||||
["交付阶段", "生成部署文档、接口文档、用户手册和变更说明", "项目组确认上线窗口、培训材料、应急预案。"],
|
||||
], [1.2, 2.8, 2.5], header_fill=LIGHT_BLUE)
|
||||
|
||||
add_heading(doc, "10. 实施计划", 1)
|
||||
add_table(doc, ["阶段", "周期", "主要产出"], [
|
||||
["阶段 0:需求深化", "3-5 周", "最终需求清单、原型、报表口径、接口清单、迁移清单。"],
|
||||
["阶段 1:平台底座", "4-6 周", "登录、权限、菜单、日志、字典、导入导出、消息中心。"],
|
||||
["阶段 2:基础与库存", "8-10 周", "基础资料、票库、票柜、库存流水、审批流程。"],
|
||||
["阶段 3:票单核心", "8-12 周", "票袋、配票、结算、重新结算、票号查询、退票。"],
|
||||
["阶段 4:扩展业务与报表", "8-10 周", "无人售、充值、特殊业务、集团/分公司/票务室报表。"],
|
||||
["阶段 5:迁移联调与上线", "8-10 周", "数据迁移、接口联调、UAT、性能测试、安全整改、上线割接。"],
|
||||
], [1.55, 1.15, 3.8])
|
||||
|
||||
add_heading(doc, "11. 测试与验收策略", 1)
|
||||
add_bullets(doc, [
|
||||
"单元测试:覆盖票号段计算、金额计算、库存扣减、状态流转、权限过滤等纯业务逻辑。",
|
||||
"接口测试:覆盖所有新增、修改、删除、审核、确认、结账、导入导出接口。",
|
||||
"场景测试:按印制到配票、配票到结算、结算到报表、退票回库等业务闭环执行。",
|
||||
"数据迁移测试:按表数量、记录数、金额合计、票号段覆盖、异常数据清单进行核对。",
|
||||
"权限测试:集团、分公司、票务室分别验证菜单、按钮、数据、导出范围。",
|
||||
"性能测试:以峰值 150 在线用户为基准,重点压测报表查询、导入、结账和库存查询。",
|
||||
])
|
||||
|
||||
add_heading(doc, "12. 风险与控制措施", 1)
|
||||
add_table(doc, ["风险", "影响", "控制措施"], [
|
||||
["报表口径未冻结", "开发完成后反复返工", "每张报表先签字确认指标、维度、过滤条件和样例数据。"],
|
||||
["历史数据质量差", "迁移延期、上线后账不平", "提前做数据体检,问题数据单独出清洗规则和责任确认。"],
|
||||
["接口资料不完整", "联调延期", "接口作为独立里程碑,未提供资料的内容不阻塞核心业务开发。"],
|
||||
["票据状态规则遗漏", "库存错误、审计风险", "核心单据全部状态机化,关键状态变更必须有流水和测试用例。"],
|
||||
["Codex 生成代码缺少业务约束", "隐藏缺陷", "Codex 只产出初稿,核心业务必须人工 review 和场景测试。"],
|
||||
["国产化兼容问题", "部署和性能不稳定", "早期引入达梦、国产 OS、中间件环境,不到上线前再适配。"],
|
||||
], [1.65, 1.8, 3.05])
|
||||
|
||||
add_heading(doc, "13. 结论建议", 1)
|
||||
add_para(doc, "建议采用“核心闭环优先、接口分阶段、报表先定口径、迁移提前演练”的实施策略。Codex 应作为研发加速工具嵌入需求整理、代码生成、测试补齐和文档交付过程,但不能替代架构决策、业务确认和验收责任。")
|
||||
add_bullets(doc, [
|
||||
"一期优先交付票务核心闭环:基础信息、票库、票柜、票袋、票单、退票、无人售、充值、报表、权限和迁移。",
|
||||
"电子签章、复杂实时接口、等保/密评配合建议单独列项,降低一期范围失控风险。",
|
||||
"开发组织建议按 10-12 人、8-10 个月主计划推进,使用 Codex 后可将重复开发和文档测试工作压缩 30%-50%。",
|
||||
"正式实施前应先输出功能清单 Excel、原型稿、数据库初设和迁移体检报告,作为报价和排期基线。",
|
||||
])
|
||||
|
||||
doc.save(OUT)
|
||||
print(OUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
252
outputs/北京公交票务评估/build_svg_diagrams.py
Normal file
@ -0,0 +1,252 @@
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path("/Users/mate/Codes/mate/mateclaw/outputs/北京公交票务评估/svg设计图")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
COLORS = {
|
||||
"red": "#D71920",
|
||||
"wall_red": "#B63A2E",
|
||||
"gray": "#6F7378",
|
||||
"dark": "#1F2933",
|
||||
"light": "#F5F7FA",
|
||||
"line": "#D8DEE6",
|
||||
"blue": "#2E86C1",
|
||||
"green": "#3FA45B",
|
||||
"yellow": "#F2C94C",
|
||||
"ink": "#0B2545",
|
||||
}
|
||||
|
||||
|
||||
def write(name: str, body: str) -> None:
|
||||
(OUT / name).write_text(body, encoding="utf-8")
|
||||
|
||||
|
||||
def header(title: str, subtitle: str, width=1440, height=960) -> str:
|
||||
return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">
|
||||
<defs>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#1F2933" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="{COLORS['red']}"/>
|
||||
</marker>
|
||||
<marker id="arrow-gray" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="{COLORS['gray']}"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title {{ font: 700 34px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: {COLORS['ink']}; }}
|
||||
.subtitle {{ font: 400 17px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: {COLORS['gray']}; }}
|
||||
.h {{ font: 700 20px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: {COLORS['ink']}; }}
|
||||
.t {{ font: 400 15px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: {COLORS['dark']}; }}
|
||||
.s {{ font: 400 13px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: {COLORS['gray']}; }}
|
||||
.tiny {{ font: 400 12px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: {COLORS['gray']}; }}
|
||||
.card {{ fill: white; stroke: {COLORS['line']}; stroke-width: 1.2; filter: url(#shadow); }}
|
||||
.band {{ fill: {COLORS['light']}; stroke: {COLORS['line']}; stroke-width: 1; }}
|
||||
.redline {{ stroke: {COLORS['red']}; stroke-width: 3; fill: none; marker-end: url(#arrow-red); }}
|
||||
.grayline {{ stroke: {COLORS['gray']}; stroke-width: 2.2; fill: none; marker-end: url(#arrow-gray); }}
|
||||
.dash {{ stroke: {COLORS['gray']}; stroke-width: 2; stroke-dasharray: 8 8; fill: none; marker-end: url(#arrow-gray); }}
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
<path d="M0 0 H1440 V10 H0 Z" fill="{COLORS['red']}"/>
|
||||
<path d="M0 10 H1440 V15 H0 Z" fill="{COLORS['gray']}" opacity="0.55"/>
|
||||
<text x="64" y="72" class="title">{title}</text>
|
||||
<text x="64" y="102" class="subtitle">{subtitle}</text>
|
||||
'''
|
||||
|
||||
|
||||
def footer() -> str:
|
||||
return f'''
|
||||
<text x="64" y="925" class="tiny">北京公交集团票务综合管理平台系统升级 | Vibe Coding 实施方案配套设计图</text>
|
||||
<circle cx="1358" cy="904" r="18" fill="{COLORS['red']}" opacity="0.95"/>
|
||||
<circle cx="1395" cy="904" r="18" fill="{COLORS['gray']}" opacity="0.78"/>
|
||||
</svg>
|
||||
'''
|
||||
|
||||
|
||||
def card(x, y, w, h, title, lines, color="#D71920"):
|
||||
parts = [f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="10" class="card"/>',
|
||||
f'<rect x="{x}" y="{y}" width="{w}" height="8" rx="4" fill="{color}"/>',
|
||||
f'<text x="{x+22}" y="{y+42}" class="h">{title}</text>']
|
||||
yy = y + 72
|
||||
for line in lines:
|
||||
parts.append(f'<text x="{x+22}" y="{yy}" class="t">{line}</text>')
|
||||
yy += 26
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
architecture = header("总体技术架构图", "从用户访问、应用服务、数据存储、外部集成到运维保障的分层架构")
|
||||
architecture += f'''
|
||||
<rect x="58" y="138" width="1324" height="680" rx="18" class="band"/>
|
||||
<text x="92" y="178" class="h">访问与用户层</text>
|
||||
{card(88, 206, 248, 138, "集团用户", ["集团管理员 / 操作员", "全局管理、报表、审批"], COLORS['red'])}
|
||||
{card(376, 206, 248, 138, "分公司用户", ["分公司管理员 / 操作员", "本公司业务、审核、统计"], COLORS['blue'])}
|
||||
{card(664, 206, 248, 138, "票务室用户", ["票柜、票袋、票单", "配票、结算、退票"], COLORS['green'])}
|
||||
{card(952, 206, 248, 138, "移动/内网终端", ["浏览器访问", "打印、导入、导出"], COLORS['yellow'])}
|
||||
|
||||
<path d="M212 356 V410" class="redline"/>
|
||||
<path d="M500 356 V410" class="redline"/>
|
||||
<path d="M788 356 V410" class="redline"/>
|
||||
<path d="M1076 356 V410" class="redline"/>
|
||||
|
||||
<text x="92" y="438" class="h">应用与业务服务层</text>
|
||||
{card(88, 466, 248, 168, "前端应用", ["Vue3 / TypeScript", "菜单导航、表单、报表", "打印与导入导出"], COLORS['red'])}
|
||||
{card(376, 466, 248, 168, "网关与认证", ["统一登录认证", "菜单 / 按钮权限", "数据范围拦截"], COLORS['gray'])}
|
||||
{card(664, 466, 248, 168, "业务服务", ["票库、票柜、票单", "充值、无人售、退票", "审批与状态机"], COLORS['blue'])}
|
||||
{card(952, 466, 248, 168, "报表服务", ["日结、月结、趋势", "Excel 导出", "汇总表加速"], COLORS['green'])}
|
||||
|
||||
<path d="M336 550 H370" class="grayline"/>
|
||||
<path d="M624 550 H658" class="grayline"/>
|
||||
<path d="M912 550 H946" class="grayline"/>
|
||||
<path d="M788 646 V698" class="redline"/>
|
||||
|
||||
<text x="92" y="724" class="h">数据、集成与运维层</text>
|
||||
{card(88, 752, 220, 120, "达梦数据库", ["主业务库 / 主备", "库存、流水、报表"], COLORS['red'])}
|
||||
{card(342, 752, 220, 120, "Redis / 缓存", ["会话、字典、短期任务", "热点查询缓存"], COLORS['gray'])}
|
||||
{card(596, 752, 220, 120, "文件与对象存储", ["模板、导入文件", "导出报表、附件"], COLORS['yellow'])}
|
||||
{card(850, 752, 220, 120, "外部系统集成", ["IC卡 / 数据湖 / 胆款", "银行清分线下导入"], COLORS['blue'])}
|
||||
{card(1104, 752, 220, 120, "运维保障", ["日志、监控、备份", "灰度、回滚、巡检"], COLORS['green'])}
|
||||
'''
|
||||
architecture += footer()
|
||||
write("01-总体技术架构图.svg", architecture)
|
||||
|
||||
|
||||
flow = header("核心票务业务流转图", "围绕票库、票柜、票袋、票单、结算、退票和报表的主业务闭环")
|
||||
flow += f'''
|
||||
<rect x="74" y="150" width="1290" height="700" rx="18" class="band"/>
|
||||
{card(104, 190, 220, 126, "1. 印制入库", ["分公司发起印制申请", "集团审核", "确认后进入票库"], COLORS['red'])}
|
||||
{card(384, 190, 220, 126, "2. 票库管理", ["票号段入库", "库存余额与流水", "调拨 / 核销 / 调账"], COLORS['gray'])}
|
||||
{card(664, 190, 220, 126, "3. 配票申请", ["票务室申请", "分公司确认", "票库到票柜"], COLORS['blue'])}
|
||||
{card(944, 190, 220, 126, "4. 票柜库存", ["票务室库存", "入库 / 出库查询", "票务室调票"], COLORS['green'])}
|
||||
|
||||
<path d="M324 253 H378" class="redline"/>
|
||||
<path d="M604 253 H658" class="redline"/>
|
||||
<path d="M884 253 H938" class="redline"/>
|
||||
|
||||
{card(104, 400, 220, 126, "5. 票袋管理", ["绑定线路、售票员", "更换线路", "票袋库存归属"], COLORS['green'])}
|
||||
{card(384, 400, 220, 126, "6. 票单配票", ["加载票袋数据", "录入起号止号", "票柜出库到票袋"], COLORS['red'])}
|
||||
{card(664, 400, 220, 126, "7. 票单结算", ["录入剩余票号", "自动计算张数金额", "生成结算记录"], COLORS['blue'])}
|
||||
{card(944, 400, 220, 126, "8. 票单查询", ["配票单 / 结算单", "打印、导出", "重新结算入口"], COLORS['gray'])}
|
||||
|
||||
<path d="M1054 328 C1054 362 260 362 214 394" class="grayline"/>
|
||||
<path d="M324 463 H378" class="redline"/>
|
||||
<path d="M604 463 H658" class="redline"/>
|
||||
<path d="M884 463 H938" class="redline"/>
|
||||
|
||||
{card(104, 610, 220, 126, "9. 退票处理", ["票袋退票", "票柜退票", "库存回写"], COLORS['wall_red'])}
|
||||
{card(384, 610, 220, 126, "10. 无人售/充值", ["无人售线路结算", "IC卡库存与收入", "导入与异常处理"], COLORS['yellow'])}
|
||||
{card(664, 610, 220, 126, "11. 报表汇总", ["日结、月结", "收入、清分、趋势", "三级数据权限"], COLORS['blue'])}
|
||||
{card(944, 610, 220, 126, "12. 审计追溯", ["库存流水", "操作日志", "票号查询"], COLORS['gray'])}
|
||||
|
||||
<path d="M214 538 V604" class="grayline"/>
|
||||
<path d="M774 538 V604" class="redline"/>
|
||||
<path d="M884 673 H938" class="grayline"/>
|
||||
<path d="M1054 598 C1054 568 1054 560 1054 538" class="dash"/>
|
||||
<text x="1178" y="557" class="s">异常更正 / 重新结算</text>
|
||||
'''
|
||||
flow += footer()
|
||||
write("02-核心票务业务流转图.svg", flow)
|
||||
|
||||
|
||||
deployment = header("部署与可运维性架构图", "面向国产化环境的部署、监控、备份、回滚和运维交接设计")
|
||||
deployment += f'''
|
||||
<rect x="70" y="148" width="1300" height="704" rx="18" class="band"/>
|
||||
<text x="102" y="188" class="h">网络与访问区</text>
|
||||
{card(104, 214, 230, 116, "用户浏览器", ["集团 / 分公司 / 票务室", "内网访问、统一认证"], COLORS['red'])}
|
||||
{card(394, 214, 230, 116, "Nginx / 网关", ["HTTPS 终止", "反向代理、限流"], COLORS['gray'])}
|
||||
{card(684, 214, 230, 116, "应用服务 A", ["业务 API", "前端静态资源"], COLORS['blue'])}
|
||||
{card(974, 214, 230, 116, "应用服务 B", ["热备 / 横向扩展", "版本灰度"], COLORS['blue'])}
|
||||
<path d="M334 272 H388" class="redline"/>
|
||||
<path d="M624 272 H678" class="redline"/>
|
||||
<path d="M914 272 H968" class="grayline"/>
|
||||
|
||||
<text x="102" y="398" class="h">数据与文件区</text>
|
||||
{card(104, 426, 230, 128, "达梦主库", ["业务数据写入", "库存与单据主数据"], COLORS['red'])}
|
||||
{card(394, 426, 230, 128, "达梦备库", ["主备同步", "故障切换准备"], COLORS['gray'])}
|
||||
{card(684, 426, 230, 128, "Redis", ["会话、缓存", "任务状态、热点字典"], COLORS['green'])}
|
||||
{card(974, 426, 230, 128, "对象存储", ["模板、导入文件", "导出报表、附件"], COLORS['yellow'])}
|
||||
<path d="M334 490 H388" class="grayline"/>
|
||||
<path d="M799 342 V420" class="redline"/>
|
||||
<path d="M1089 342 V420" class="grayline"/>
|
||||
|
||||
<text x="102" y="620" class="h">运维保障区</text>
|
||||
{card(104, 648, 216, 124, "日志中心", ["业务日志、接口日志", "登录与操作审计"], COLORS['gray'])}
|
||||
{card(360, 648, 216, 124, "监控告警", ["存活、慢SQL、磁盘", "异常率、任务失败"], COLORS['red'])}
|
||||
{card(616, 648, 216, 124, "备份恢复", ["数据库全量/增量", "文件备份、恢复演练"], COLORS['green'])}
|
||||
{card(872, 648, 216, 124, "CI/CD", ["构建、测试、制品", "部署、版本标记"], COLORS['blue'])}
|
||||
{card(1128, 648, 216, 124, "回滚预案", ["旧版本保留", "割接窗口、冒烟检查"], COLORS['wall_red'])}
|
||||
<path d="M684 592 C550 610 465 620 468 642" class="dash"/>
|
||||
<path d="M914 592 C790 610 724 622 724 642" class="dash"/>
|
||||
<path d="M624 272 C520 340 475 380 480 420" class="dash"/>
|
||||
'''
|
||||
deployment += footer()
|
||||
write("03-部署与可运维性架构图.svg", deployment)
|
||||
|
||||
|
||||
permissions = header("权限与数据范围设计图", "菜单权限、按钮权限、接口权限、数据权限和导出权限的统一控制")
|
||||
permissions += f'''
|
||||
<rect x="70" y="148" width="1300" height="704" rx="18" class="band"/>
|
||||
{card(96, 198, 240, 134, "集团管理员", ["配置菜单、角色、用户", "分配分公司数据范围", "查看全集团数据"], COLORS['red'])}
|
||||
{card(396, 198, 240, 134, "集团操作员", ["按角色使用功能", "查看授权公司数据", "集团级报表"], COLORS['gray'])}
|
||||
{card(696, 198, 240, 134, "分公司管理员", ["维护本公司用户", "配置票务室权限", "本公司业务管理"], COLORS['blue'])}
|
||||
{card(996, 198, 240, 134, "票务室操作员", ["票柜、票袋、票单", "结算、退票、导出", "仅本票务室数据"], COLORS['green'])}
|
||||
|
||||
<rect x="170" y="414" width="1100" height="100" rx="12" fill="#FFFFFF" stroke="{COLORS['line']}" filter="url(#shadow)"/>
|
||||
<text x="212" y="456" class="h">统一权限拦截层</text>
|
||||
<text x="212" y="486" class="t">菜单权限 → 按钮权限 → 接口权限 → 数据范围权限 → 导出/打印权限</text>
|
||||
<path d="M216 344 V408" class="redline"/>
|
||||
<path d="M516 344 V408" class="grayline"/>
|
||||
<path d="M816 344 V408" class="grayline"/>
|
||||
<path d="M1116 344 V408" class="grayline"/>
|
||||
|
||||
{card(116, 604, 230, 126, "组织维度", ["集团", "分公司", "票务室 / 车队 / 线路"], COLORS['red'])}
|
||||
{card(386, 604, 230, 126, "业务维度", ["票库 / 票柜 / 票袋", "充值网点", "无人售线路"], COLORS['blue'])}
|
||||
{card(656, 604, 230, 126, "数据维度", ["单据、库存、流水", "报表汇总", "历史迁移数据"], COLORS['green'])}
|
||||
{card(926, 604, 230, 126, "审计维度", ["登录日志", "操作日志", "导入导出日志"], COLORS['gray'])}
|
||||
<path d="M720 524 V598" class="redline"/>
|
||||
<path d="M720 524 C530 548 500 570 500 598" class="grayline"/>
|
||||
<path d="M720 524 C902 548 1040 570 1040 598" class="grayline"/>
|
||||
'''
|
||||
permissions += footer()
|
||||
write("04-权限与数据范围设计图.svg", permissions)
|
||||
|
||||
|
||||
delivery = header("Vibe Coding 协同交付流程图", "把大系统拆成可验证的小任务,形成需求、编码、测试、评审、交付闭环")
|
||||
delivery += f'''
|
||||
<rect x="74" y="148" width="1290" height="704" rx="18" class="band"/>
|
||||
{card(102, 202, 230, 128, "1. 需求卡片化", ["按页面/动作拆分", "角色、输入、输出", "验收标准"], COLORS['red'])}
|
||||
{card(382, 202, 230, 128, "2. 领域建模", ["状态机、票号段", "库存流水、报表口径", "人工确认"], COLORS['gray'])}
|
||||
{card(662, 202, 230, 128, "3. 任务生成", ["表结构、接口", "页面、测试", "小批量执行"], COLORS['blue'])}
|
||||
{card(942, 202, 230, 128, "4. 编码实现", ["前后端协同", "导入导出", "权限与日志"], COLORS['green'])}
|
||||
<path d="M332 266 H376" class="redline"/>
|
||||
<path d="M612 266 H656" class="redline"/>
|
||||
<path d="M892 266 H936" class="redline"/>
|
||||
|
||||
{card(102, 470, 230, 128, "8. 模块验收", ["业务演示", "样例数据核对", "问题闭环"], COLORS['wall_red'])}
|
||||
{card(382, 470, 230, 128, "7. 人工评审", ["核心规则 review", "安全与权限检查", "SQL 与事务检查"], COLORS['gray'])}
|
||||
{card(662, 470, 230, 128, "6. 自动化测试", ["单元 / 接口", "场景 / 回归", "构建检查"], COLORS['blue'])}
|
||||
{card(942, 470, 230, 128, "5. 本地自测", ["页面联调", "异常分支", "导入导出校验"], COLORS['green'])}
|
||||
<path d="M1057 342 V464" class="redline"/>
|
||||
<path d="M942 534 H898" class="grayline"/>
|
||||
<path d="M662 534 H618" class="grayline"/>
|
||||
<path d="M382 534 H338" class="grayline"/>
|
||||
|
||||
<rect x="208" y="690" width="944" height="74" rx="12" fill="#FFFFFF" stroke="{COLORS['line']}" filter="url(#shadow)"/>
|
||||
<text x="238" y="726" class="h">交付原则</text>
|
||||
<text x="362" y="724" class="t">小任务、强测试、人工把关、持续演示;常规功能提效,核心规则不省评审。</text>
|
||||
<path d="M217 608 C217 670 678 650 678 684" class="dash"/>
|
||||
'''
|
||||
delivery += footer()
|
||||
write("05-VibeCoding协同交付流程图.svg", delivery)
|
||||
|
||||
index = "\n".join([
|
||||
"# SVG 设计图清单",
|
||||
"",
|
||||
"- 01-总体技术架构图.svg",
|
||||
"- 02-核心票务业务流转图.svg",
|
||||
"- 03-部署与可运维性架构图.svg",
|
||||
"- 04-权限与数据范围设计图.svg",
|
||||
"- 05-VibeCoding协同交付流程图.svg",
|
||||
])
|
||||
(OUT / "README.md").write_text(index, encoding="utf-8")
|
||||
print(OUT)
|
||||
475
outputs/北京公交票务评估/build_vibe_design_doc.py
Normal file
@ -0,0 +1,475 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from re import sub
|
||||
from typing import Iterable
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.section import WD_ORIENT, WD_SECTION
|
||||
from docx.enum.table import WD_ALIGN_VERTICAL, WD_TABLE_ALIGNMENT
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Inches, Pt, RGBColor
|
||||
|
||||
|
||||
SOURCE = Path("/Users/mate/Docs/Work/北京公交集团票务综合管理平台系统升级-需求分析说明书(初稿0506).docx")
|
||||
OUT = Path("/Users/mate/Codes/mate/mateclaw/outputs/北京公交票务评估/北京公交集团票务综合管理平台系统升级-VibeCoding实施方案与工作量清单.docx")
|
||||
|
||||
BLUE = RGBColor(46, 116, 181)
|
||||
DARK_BLUE = RGBColor(31, 77, 120)
|
||||
INK = RGBColor(11, 37, 69)
|
||||
MUTED = RGBColor(96, 108, 122)
|
||||
LIGHT = "F2F4F7"
|
||||
LIGHT_BLUE = "E8EEF5"
|
||||
CALLOUT = "F4F6F9"
|
||||
|
||||
|
||||
def clean(text: str) -> str:
|
||||
text = text.replace("\u200c", "").replace("\u200b", "").replace("\ufeff", "")
|
||||
text = sub(r"\s+", " ", text).strip()
|
||||
return text
|
||||
|
||||
|
||||
def module_name(text: str) -> str:
|
||||
text = clean(text)
|
||||
text = sub(r"^[\d.、\s]+", "", text)
|
||||
return text.strip(" ")
|
||||
|
||||
|
||||
def set_run_font(run, size=None, bold=None, color=None, name="Calibri"):
|
||||
run.font.name = name
|
||||
run._element.rPr.rFonts.set(qn("w:ascii"), name)
|
||||
run._element.rPr.rFonts.set(qn("w:hAnsi"), name)
|
||||
run._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
|
||||
if size is not None:
|
||||
run.font.size = Pt(size)
|
||||
if bold is not None:
|
||||
run.bold = bold
|
||||
if color is not None:
|
||||
run.font.color.rgb = color
|
||||
|
||||
|
||||
def shade(cell, fill: str):
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
shd = tc_pr.find(qn("w:shd"))
|
||||
if shd is None:
|
||||
shd = OxmlElement("w:shd")
|
||||
tc_pr.append(shd)
|
||||
shd.set(qn("w:fill"), fill)
|
||||
|
||||
|
||||
def margins(cell, top=80, start=120, bottom=80, end=120):
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
||||
if tc_mar is None:
|
||||
tc_mar = OxmlElement("w:tcMar")
|
||||
tc_pr.append(tc_mar)
|
||||
for key, val in [("top", top), ("start", start), ("bottom", bottom), ("end", end)]:
|
||||
node = tc_mar.find(qn(f"w:{key}"))
|
||||
if node is None:
|
||||
node = OxmlElement(f"w:{key}")
|
||||
tc_mar.append(node)
|
||||
node.set(qn("w:w"), str(val))
|
||||
node.set(qn("w:type"), "dxa")
|
||||
|
||||
|
||||
def table_width(table, widths: list[float]):
|
||||
table.autofit = False
|
||||
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
for row in table.rows:
|
||||
for idx, cell in enumerate(row.cells):
|
||||
cell.width = Inches(widths[idx])
|
||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
||||
margins(cell)
|
||||
|
||||
|
||||
def add_para(doc, text="", bold=False, color=None, size=10.5, align=None):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
p.paragraph_format.line_spacing = 1.12
|
||||
if align is not None:
|
||||
p.alignment = align
|
||||
if text:
|
||||
r = p.add_run(text)
|
||||
set_run_font(r, size=size, bold=bold, color=color)
|
||||
return p
|
||||
|
||||
|
||||
def heading(doc, text, level):
|
||||
p = doc.add_heading(text, level=level)
|
||||
p.paragraph_format.space_before = Pt(16 if level == 1 else 10)
|
||||
p.paragraph_format.space_after = Pt(8 if level == 1 else 5)
|
||||
for r in p.runs:
|
||||
set_run_font(r, size={1: 16, 2: 13, 3: 12}.get(level, 11), bold=True, color=BLUE if level < 3 else DARK_BLUE)
|
||||
|
||||
|
||||
def bullets(doc, items: Iterable[str]):
|
||||
for item in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.paragraph_format.line_spacing = 1.12
|
||||
r = p.add_run(item)
|
||||
set_run_font(r, size=10)
|
||||
|
||||
|
||||
def numbered(doc, items: Iterable[str]):
|
||||
for item in items:
|
||||
p = doc.add_paragraph(style="List Number")
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.paragraph_format.line_spacing = 1.12
|
||||
r = p.add_run(item)
|
||||
set_run_font(r, size=10)
|
||||
|
||||
|
||||
def add_table(doc, headers: list[str], rows: list[list[str]], widths: list[float], fill=LIGHT, font_size=8.8):
|
||||
table = doc.add_table(rows=1, cols=len(headers))
|
||||
table.style = "Table Grid"
|
||||
for i, h in enumerate(headers):
|
||||
c = table.rows[0].cells[i]
|
||||
c.text = h
|
||||
shade(c, fill)
|
||||
margins(c)
|
||||
for p in c.paragraphs:
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
for r in p.runs:
|
||||
set_run_font(r, size=font_size, bold=True, color=INK)
|
||||
for row in rows:
|
||||
cells = table.add_row().cells
|
||||
for i, value in enumerate(row):
|
||||
cells[i].text = str(value)
|
||||
margins(cells[i])
|
||||
for p in cells[i].paragraphs:
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
p.paragraph_format.line_spacing = 1.02
|
||||
for r in p.runs:
|
||||
set_run_font(r, size=font_size)
|
||||
table_width(table, widths)
|
||||
doc.add_paragraph()
|
||||
|
||||
|
||||
def callout(doc, title, body):
|
||||
table = doc.add_table(rows=1, cols=1)
|
||||
table.style = "Table Grid"
|
||||
cell = table.cell(0, 0)
|
||||
shade(cell, CALLOUT)
|
||||
margins(cell, top=120, bottom=120, start=160, end=160)
|
||||
p = cell.paragraphs[0]
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(title)
|
||||
set_run_font(r, size=10.5, bold=True, color=DARK_BLUE)
|
||||
p2 = cell.add_paragraph()
|
||||
p2.paragraph_format.space_after = Pt(0)
|
||||
r2 = p2.add_run(body)
|
||||
set_run_font(r2, size=10)
|
||||
table_width(table, [6.5])
|
||||
doc.add_paragraph()
|
||||
|
||||
|
||||
def configure(doc: Document):
|
||||
section = doc.sections[0]
|
||||
section.top_margin = Inches(0.85)
|
||||
section.bottom_margin = Inches(0.85)
|
||||
section.left_margin = Inches(0.9)
|
||||
section.right_margin = Inches(0.9)
|
||||
section.header_distance = Inches(0.45)
|
||||
section.footer_distance = Inches(0.45)
|
||||
styles = doc.styles
|
||||
normal = styles["Normal"]
|
||||
normal.font.name = "Calibri"
|
||||
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
|
||||
normal.font.size = Pt(10.5)
|
||||
normal.paragraph_format.space_after = Pt(6)
|
||||
normal.paragraph_format.line_spacing = 1.12
|
||||
for style_name, size, color in [("Heading 1", 16, BLUE), ("Heading 2", 13, BLUE), ("Heading 3", 12, DARK_BLUE)]:
|
||||
st = styles[style_name]
|
||||
st.font.name = "Calibri"
|
||||
st._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
|
||||
st.font.size = Pt(size)
|
||||
st.font.bold = True
|
||||
st.font.color.rgb = color
|
||||
header = section.header.paragraphs[0]
|
||||
header.text = "北京公交集团票务综合管理平台系统升级 | Vibe Coding 实施与工作量评估"
|
||||
for r in header.runs:
|
||||
set_run_font(r, size=8.5, color=MUTED)
|
||||
footer = section.footer.paragraphs[0]
|
||||
footer.text = "内部评估文件"
|
||||
footer.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||
for r in footer.runs:
|
||||
set_run_font(r, size=8.5, color=MUTED)
|
||||
|
||||
|
||||
def extract_items():
|
||||
doc = Document(SOURCE)
|
||||
table = doc.tables[38]
|
||||
items = []
|
||||
current = ""
|
||||
for row in table.rows[1:]:
|
||||
cells = [clean(c.text) for c in row.cells[:5]]
|
||||
mod, page, func, change, note = cells
|
||||
if mod:
|
||||
current = module_name(mod)
|
||||
if not page:
|
||||
continue
|
||||
items.append({
|
||||
"module": current,
|
||||
"page": page,
|
||||
"function": func,
|
||||
"change": change,
|
||||
"note": note,
|
||||
})
|
||||
return items
|
||||
|
||||
|
||||
def classify(item):
|
||||
text = f"{item['module']} {item['page']} {item['function']} {item['change']} {item['note']}"
|
||||
if "可删除" in text or "删除" in item["page"] or item["change"] == "删除":
|
||||
return "拟删除/整合", 0, 1, "确认下线、菜单隐藏、历史数据保留和跳转清理。"
|
||||
if any(k in text for k in ["结算", "重新结算", "配票", "调票", "退票", "库存", "入库", "出库", "核销", "调拨", "印制", "票号"]):
|
||||
lo, hi = 8, 18
|
||||
if any(k in text for k in ["票单结算", "重新结算", "票袋调票", "票柜退票", "票袋退票"]):
|
||||
lo, hi = 14, 26
|
||||
return "核心业务", lo, hi, "需要状态机、库存流水、票号段校验、事务一致性和回归测试。"
|
||||
if any(k in item["module"] for k in ["统计报表"]) or "报表" in text or "统计" in text:
|
||||
return "报表统计", 6, 14, "需确认统计口径、权限范围、导出格式和历史数据性能。"
|
||||
if "导入" in text or "上传" in text or "下载" in text:
|
||||
return "导入导出", 5, 10, "需模板、字段校验、失败明细、导入日志和重复处理。"
|
||||
if any(k in item["module"] for k in ["系统管理"]) or any(k in item["page"] for k in ["用户", "权限", "菜单", "日志", "阈值"]):
|
||||
return "平台能力", 6, 14, "需统一认证授权、按钮权限、数据范围和审计记录。"
|
||||
if any(k in text for k in ["查询、新建、修改、删除", "添加、编辑、删除", "添加、编辑", "新增", "管理"]):
|
||||
return "常规功能", 4, 8, "以列表、表单、校验、权限按钮和操作日志为主。"
|
||||
return "查询维护", 3, 6, "以查询、筛选、详情、导出或基础维护为主。"
|
||||
|
||||
|
||||
def build_rows(items):
|
||||
rows = []
|
||||
for idx, item in enumerate(items, start=1):
|
||||
kind, lo, hi, detail = classify(item)
|
||||
vibe_lo = round(lo * 0.65, 1)
|
||||
vibe_hi = round(hi * 0.75, 1)
|
||||
dev = build_dev_scope(item, kind)
|
||||
rows.append({
|
||||
**item,
|
||||
"idx": idx,
|
||||
"kind": kind,
|
||||
"lo": lo,
|
||||
"hi": hi,
|
||||
"vibe_lo": vibe_lo,
|
||||
"vibe_hi": vibe_hi,
|
||||
"dev": dev,
|
||||
"detail": detail,
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def build_dev_scope(item, kind):
|
||||
page = item["page"]
|
||||
if kind == "拟删除/整合":
|
||||
return f"确认 {page} 下线或合并路径,处理菜单、权限、历史查询入口和数据保留。"
|
||||
if kind == "核心业务":
|
||||
return f"建设 {page} 的单据、审批/确认、库存流水、票号段校验、查询导出和异常回滚。"
|
||||
if kind == "报表统计":
|
||||
return f"建设 {page} 查询、统计聚合、权限过滤、Excel 导出、必要趋势展示和口径校验。"
|
||||
if kind == "导入导出":
|
||||
return f"建设 {page} 模板下载、文件上传、字段校验、入库处理、失败明细和导入日志。"
|
||||
if kind == "平台能力":
|
||||
return f"建设 {page} 的配置管理、权限控制、审计记录、查询筛选和维护页面。"
|
||||
return f"建设 {page} 的列表查询、详情、新增/编辑/停用或删除、权限按钮、日志和导出能力。"
|
||||
|
||||
|
||||
def module_summary(rows):
|
||||
result = []
|
||||
grouped = defaultdict(list)
|
||||
for row in rows:
|
||||
grouped[row["module"]].append(row)
|
||||
for mod, vals in grouped.items():
|
||||
lo = sum(v["lo"] for v in vals)
|
||||
hi = sum(v["hi"] for v in vals)
|
||||
vlo = sum(v["vibe_lo"] for v in vals)
|
||||
vhi = sum(v["vibe_hi"] for v in vals)
|
||||
result.append([mod, str(len(vals)), f"{lo}-{hi}", f"{round(vlo)}-{round(vhi)}", module_focus(mod)])
|
||||
return result
|
||||
|
||||
|
||||
def module_focus(mod):
|
||||
if "票单" in mod:
|
||||
return "票袋、配票、结算、重新结算、票号追溯,是核心复杂域。"
|
||||
if "票柜" in mod or "票库" in mod or "申请" in mod or "审核" in mod:
|
||||
return "围绕库存流转、审批状态和票号段控制。"
|
||||
if "统计报表" in mod:
|
||||
return "以口径确认、聚合性能、导出格式和权限过滤为重点。"
|
||||
if "系统" in mod:
|
||||
return "全局权限、菜单、日志、阈值,是平台底座。"
|
||||
if "基础" in mod:
|
||||
return "主数据维护和外部同步,是后续业务依赖。"
|
||||
return "按业务场景完成查询、维护、导入导出和日志审计。"
|
||||
|
||||
|
||||
def make_landscape(doc):
|
||||
section = doc.add_section(WD_SECTION.NEW_PAGE)
|
||||
section.orientation = WD_ORIENT.LANDSCAPE
|
||||
section.page_width, section.page_height = section.page_height, section.page_width
|
||||
section.top_margin = Inches(0.6)
|
||||
section.bottom_margin = Inches(0.6)
|
||||
section.left_margin = Inches(0.55)
|
||||
section.right_margin = Inches(0.55)
|
||||
section.header_distance = Inches(0.35)
|
||||
section.footer_distance = Inches(0.35)
|
||||
return section
|
||||
|
||||
|
||||
def build():
|
||||
items = extract_items()
|
||||
rows = build_rows(items)
|
||||
normal_lo = sum(r["lo"] for r in rows)
|
||||
normal_hi = sum(r["hi"] for r in rows)
|
||||
vibe_lo = round(sum(r["vibe_lo"] for r in rows))
|
||||
vibe_hi = round(sum(r["vibe_hi"] for r in rows))
|
||||
|
||||
doc = Document()
|
||||
configure(doc)
|
||||
|
||||
add_para(doc, "项目实施方案", bold=True, color=MUTED, size=11)
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run("北京公交集团票务综合管理平台系统升级")
|
||||
set_run_font(r, size=23, bold=True, color=INK)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run("Vibe Coding 实施方案与工作量清单")
|
||||
set_run_font(r, size=15, color=MUTED)
|
||||
p.paragraph_format.space_after = Pt(14)
|
||||
add_table(doc, ["字段", "内容"], [
|
||||
["文档用途", "用于项目立项、排期、报价拆分、研发组织和验收范围确认"],
|
||||
["编制日期", date.today().isoformat()],
|
||||
["需求来源", "北京公交集团票务综合管理平台系统升级需求分析说明书(初稿0506)"],
|
||||
["估算口径", "包含需求深化、设计、开发、自测、联调、迁移、测试配合和上线支持"],
|
||||
], [1.35, 5.15], fill=LIGHT_BLUE, font_size=9)
|
||||
|
||||
callout(doc, "简要结论", f"本项目按完整重构口径约 {len(rows)} 个功能项,正常开发工作量约 {normal_lo}-{normal_hi} 人日。采用 Vibe Coding 协同方式后,可将重复代码、测试样例、文档和脚手架类工作压缩,执行工作量约 {vibe_lo}-{vibe_hi} 人日,但票据规则、报表口径、数据迁移和上线验收仍需人工主导。")
|
||||
|
||||
heading(doc, "一、简要评估", 1)
|
||||
add_table(doc, ["项目", "建议口径"], [
|
||||
["功能规模", f"{len(rows)} 个功能项,覆盖 14 个一级模块,含基础资料、票库、票柜、票单、退票、充值、报表、系统管理等。"],
|
||||
["正常团队", "10-12 人,8-10 个月完成;包含产品、前端、后端、数据迁移、测试、实施。"],
|
||||
["Vibe Coding 团队", "7-9 人,7-8 个月完成;前提是需求冻结、接口材料及时、测试自动化和人工评审到位。"],
|
||||
["核心难点", "票号段连续性、库存流水、票单结算、报表口径、数据权限、历史数据迁移、国产化适配。"],
|
||||
["报价建议", "完整建设建议按 450-650 万区间,稳妥报价点 520-580 万;电子签章、等保测评、第三方授权另列。"],
|
||||
], [1.4, 5.1], fill=LIGHT_BLUE, font_size=9)
|
||||
|
||||
heading(doc, "二、模块工作量简表", 1)
|
||||
add_table(doc, ["模块", "功能项数", "正常人日", "协同后人日", "重点说明"], module_summary(rows), [1.25, 0.65, 0.85, 0.9, 2.85], fill=LIGHT)
|
||||
|
||||
heading(doc, "三、Vibe Coding 实施方式", 1)
|
||||
add_para(doc, "本方案中的 Vibe Coding 不是直接让工具一次性生成完整系统,而是把需求、设计、编码、测试、文档拆成小批量可验证任务。每个功能都按照“需求卡片 - 数据结构 - 接口 - 页面 - 测试 - 评审 - 合并”的节奏推进。")
|
||||
numbered(doc, [
|
||||
"需求卡片化:每个页面或业务动作形成独立任务,明确角色、数据范围、输入输出、状态和验收标准。",
|
||||
"先底座后业务:先完成认证、菜单、权限、字典、日志、导入导出、文件存储等平台能力。",
|
||||
"核心域手工建模:票据库存、票号段、结算、退票、调账等领域模型由架构师和业务人员先确认,再进入编码。",
|
||||
"批量生成常规代码:常规列表、表单、接口、DTO、Mapper、权限按钮、导入模板可以批量生成并统一 review。",
|
||||
"测试前置:每个核心业务先写状态流转和金额/张数计算测试,避免只实现页面不验证账务逻辑。",
|
||||
"小步交付:每个模块独立演示、独立验收、独立回归,不把问题积压到总体验收阶段。",
|
||||
])
|
||||
|
||||
heading(doc, "四、实施步骤简表", 1)
|
||||
add_table(doc, ["阶段", "周期", "主要工作", "交付物"], [
|
||||
["0. 需求深化", "3-5 周", "需求清单、业务流程、角色权限、报表口径、接口范围、迁移对象确认。", "需求矩阵、原型、报表口径表、接口清单、迁移清单。"],
|
||||
["1. 平台底座", "4-6 周", "登录、用户、角色、菜单、数据权限、日志、字典、阈值、消息、文件导入导出。", "可登录可授权的基础系统。"],
|
||||
["2. 基础与库存", "8-10 周", "基础资料、票库、票柜、审批、库存余额和流水。", "库存闭环 MVP。"],
|
||||
["3. 票单核心", "8-12 周", "票袋、配票、结算、重新结算、退票、票号查询。", "票务室核心业务闭环。"],
|
||||
["4. 扩展业务与报表", "8-10 周", "无人售、充值、特殊业务、集团/分公司/票务室报表。", "完整业务与统计能力。"],
|
||||
["5. 联调上线", "8-10 周", "数据迁移、接口联调、UAT、性能、安全整改、上线演练。", "生产上线版本与运维交接材料。"],
|
||||
], [0.9, 0.8, 3.05, 1.75], fill=LIGHT)
|
||||
|
||||
heading(doc, "五、关键设计细节", 1)
|
||||
heading(doc, "5.1 库存与票号设计", 2)
|
||||
bullets(doc, [
|
||||
"采用“库存余额表 + 库存流水表 + 业务单据表”三层模型,不以单一库存字段承载全部业务历史。",
|
||||
"票号段字段统一包含票价、票种、票组、起号、止号、张数、状态、库存主体、来源单据。",
|
||||
"所有出入库动作必须写流水,流水记录来源、目标、数量、金额、操作人、操作时间和业务原因。",
|
||||
"连续票号段需校验重叠、断号、越界、已核销、已退票、已结算等状态。",
|
||||
"重新结算不得覆盖原记录,应形成更正单据和差异流水,保证历史可追溯。",
|
||||
])
|
||||
heading(doc, "5.2 权限与数据范围", 2)
|
||||
bullets(doc, [
|
||||
"权限拆分为菜单权限、按钮权限、接口权限、数据范围权限和导出权限。",
|
||||
"集团管理员可分配全局权限;集团操作员按授权查看部分分公司;分公司角色默认本分公司;票务室角色只看本票务室。",
|
||||
"后端查询必须统一注入数据范围条件,不能只依赖前端隐藏菜单。",
|
||||
"导出、打印、报表接口必须复用同一套数据权限规则。",
|
||||
])
|
||||
heading(doc, "5.3 报表设计", 2)
|
||||
bullets(doc, [
|
||||
"每张报表上线前先确认指标定义、筛选条件、统计维度、取数来源、金额精度、导出格式。",
|
||||
"集团和分公司同类报表尽量合并,用数据权限和维度字段控制展示结果。",
|
||||
"高频报表建立日汇总/月汇总表,避免每次查询实时扫大量流水。",
|
||||
"报表验收必须使用旧系统样例数据做金额和张数比对。",
|
||||
])
|
||||
heading(doc, "5.4 数据迁移", 2)
|
||||
bullets(doc, [
|
||||
"迁移对象包括 Oracle 表、序列、视图、函数、历史业务数据和必要附件。",
|
||||
"先做数据体检,输出重复票号、缺失组织、异常状态、金额不平、非法日期等问题清单。",
|
||||
"至少执行三轮迁移演练:开发、测试、准生产,每轮生成记录数、金额、票号覆盖和异常清单。",
|
||||
"上线割接需要冻结旧系统写入、执行增量迁移、业务抽样核验,并保留回滚方案。",
|
||||
])
|
||||
|
||||
heading(doc, "六、部署与可运维性设计", 1)
|
||||
add_para(doc, "本项目属于票务核心管理系统,部署和运维能力应在一期同步设计,不能等到开发完成后补。运维设计目标是:可部署、可监控、可审计、可备份、可恢复、可灰度、可回滚。")
|
||||
add_table(doc, ["运维领域", "设计要求", "工作量建议"], [
|
||||
["环境规划", "至少规划开发、测试、预生产、生产四类环境;生产采用应用服务与数据库分离部署,数据库主备。", "12-18 人日"],
|
||||
["国产化适配", "适配达梦数据库、国产服务器操作系统、国产中间件或 Nginx/应用服务部署要求,提前验证驱动和 SQL 兼容性。", "20-35 人日"],
|
||||
["配置管理", "数据库连接、文件存储、外部接口、导入目录、日志级别、任务开关全部配置化,避免写死在代码中。", "8-12 人日"],
|
||||
["CI/CD", "建立构建、单元测试、打包、制品归档、部署脚本流程;生产部署需支持版本标记和回滚。", "15-25 人日"],
|
||||
["日志审计", "区分业务操作日志、登录日志、接口日志、导入导出日志、系统错误日志;关键单据状态变化必须可追溯。", "15-25 人日"],
|
||||
["监控告警", "监控应用存活、接口耗时、数据库连接池、慢 SQL、磁盘空间、导入任务、定时任务和异常错误率。", "15-25 人日"],
|
||||
["备份恢复", "数据库全量/增量备份,导入文件和导出附件备份,定期恢复演练,形成恢复时间目标。", "12-20 人日"],
|
||||
["上线回滚", "上线前冻结窗口、迁移脚本、冒烟检查、业务抽样、失败回滚、旧系统只读策略均需预案。", "15-25 人日"],
|
||||
["运维交接", "提供部署手册、配置手册、巡检手册、常见问题、数据恢复手册和接口联调手册。", "10-18 人日"],
|
||||
], [1.15, 4.2, 1.15], fill=LIGHT_BLUE)
|
||||
callout(doc, "部署工作量口径", "部署与运维建设建议单独预留 120-200 人日。如果甲方要求等保测评、密评配合、信创测评、双活容灾或第三方运维平台对接,应另行增加专项工作量和报价。")
|
||||
|
||||
heading(doc, "七、详细功能项清单", 1)
|
||||
add_para(doc, "以下清单按需求文档附录逐项展开。正常人日为完整开发、自测、联调配合口径;协同后人日为采用 Vibe Coding 方式后的执行估算,不包含甲方等待、接口资料延迟和重大需求变更。")
|
||||
make_landscape(doc)
|
||||
heading(doc, "详细功能项与工作量估算", 1)
|
||||
detail_rows = []
|
||||
for r in rows:
|
||||
detail_rows.append([
|
||||
r["idx"],
|
||||
r["module"],
|
||||
r["page"],
|
||||
r["kind"],
|
||||
r["function"] or "按页面说明",
|
||||
r["dev"],
|
||||
f"{r['lo']}-{r['hi']}",
|
||||
f"{r['vibe_lo']}-{r['vibe_hi']}",
|
||||
r["detail"],
|
||||
])
|
||||
add_table(
|
||||
doc,
|
||||
["序号", "模块", "功能页面", "类型", "现有功能", "开发内容", "正常人日", "协同后", "难点/备注"],
|
||||
detail_rows,
|
||||
[0.35, 0.95, 1.35, 0.75, 1.25, 2.1, 0.58, 0.58, 1.6],
|
||||
fill=LIGHT_BLUE,
|
||||
font_size=7.2,
|
||||
)
|
||||
|
||||
doc.add_section(WD_SECTION.NEW_PAGE)
|
||||
heading(doc, "八、风险与边界", 1)
|
||||
add_table(doc, ["风险点", "表现", "建议处理"], [
|
||||
["需求边界不清", "文档中存在“待明确”“删除”“整合”项,容易反复变更。", "在开发前形成冻结版功能清单,变更走审批和工期调整。"],
|
||||
["接口资料缺失", "数据湖、二维码、胆款、电子签章等接口材料未完全明确。", "一期只做接口框架和已确认接口,未确认部分单独列二期。"],
|
||||
["报表口径争议", "同一报表集团/分公司/票务室口径不同,数字验收困难。", "先用旧系统样例数据签字确认,再开发。"],
|
||||
["历史数据异常", "旧系统长期运行导致脏数据和状态不一致。", "迁移前做数据体检,异常数据由业务确认处理规则。"],
|
||||
["AI 生成代码质量波动", "常规代码效率高,但核心业务可能遗漏隐性规则。", "核心交易类代码必须人工 review,并要求测试覆盖状态流转和金额计算。"],
|
||||
], [1.35, 2.45, 2.7], fill=LIGHT)
|
||||
|
||||
heading(doc, "九、结论", 1)
|
||||
add_para(doc, f"本项目完整实施建议按 10-12 人、8-10 个月规划;采用 Vibe Coding 协同后,可把常规代码、测试样例、文档和重复性页面工作压缩,推荐按 7-9 人、7-8 个月作为进取计划。部署与可运维性需同步纳入一期,额外预留 120-200 人日。即便采用协同开发,票据库存、票单结算、报表口径、历史数据迁移和上线割接仍是项目成败关键,需要架构师、业务负责人、测试负责人和运维负责人持续把关。")
|
||||
|
||||
doc.save(OUT)
|
||||
print(OUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
70
outputs/北京公交票务评估/insert_diagrams_into_doc.py
Normal file
@ -0,0 +1,70 @@
|
||||
from pathlib import Path
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.shared import Inches, Pt
|
||||
|
||||
|
||||
SRC = Path("/Users/mate/Codes/mate/mateclaw/outputs/北京公交票务评估/北京公交集团票务综合管理平台系统升级-VibeCoding实施方案与工作量清单.docx")
|
||||
OUT = Path("/Users/mate/Codes/mate/mateclaw/outputs/北京公交票务评估/北京公交集团票务综合管理平台系统升级-VibeCoding实施方案与工作量清单-含设计图.docx")
|
||||
PNG = Path("/Users/mate/Codes/mate/mateclaw/outputs/北京公交票务评估/svg设计图/png预览")
|
||||
|
||||
|
||||
DIAGRAMS = {
|
||||
"三、Vibe Coding 实施方式": ("05-VibeCoding协同交付流程图.png", "图 1 Vibe Coding 协同交付流程图"),
|
||||
"五、关键设计细节": ("02-核心票务业务流转图.png", "图 2 核心票务业务流转图"),
|
||||
"5.2 权限与数据范围": ("04-权限与数据范围设计图.png", "图 3 权限与数据范围设计图"),
|
||||
"六、部署与可运维性设计": ("03-部署与可运维性架构图.png", "图 4 部署与可运维性架构图"),
|
||||
"七、详细功能项清单": ("01-总体技术架构图.png", "图 5 总体技术架构图"),
|
||||
}
|
||||
|
||||
|
||||
def insert_after(paragraph, image_path: Path, caption: str, width=Inches(6.7)):
|
||||
caption_p = paragraph.insert_paragraph_before("")
|
||||
# Move caption and picture after the heading by inserting before next paragraph is not directly
|
||||
# supported in python-docx. Use low-level insertion after heading.
|
||||
pic_p = paragraph._p.addnext(paragraph._p.__class__())
|
||||
|
||||
|
||||
def add_picture_after(paragraph, image_path: Path, caption: str, width=Inches(6.7)):
|
||||
doc = paragraph.part.document
|
||||
p_pic = doc.add_paragraph()
|
||||
p_pic.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = p_pic.add_run()
|
||||
run.add_picture(str(image_path), width=width)
|
||||
p_pic.paragraph_format.space_before = Pt(6)
|
||||
p_pic.paragraph_format.space_after = Pt(3)
|
||||
|
||||
p_cap = doc.add_paragraph()
|
||||
p_cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p_cap.paragraph_format.space_after = Pt(8)
|
||||
r = p_cap.add_run(caption)
|
||||
r.font.size = Pt(9)
|
||||
r.font.name = "Microsoft YaHei"
|
||||
|
||||
# Move the newly appended picture + caption immediately after the target paragraph.
|
||||
paragraph._p.addnext(p_cap._p)
|
||||
paragraph._p.addnext(p_pic._p)
|
||||
|
||||
|
||||
def main():
|
||||
doc = Document(SRC)
|
||||
inserted = 0
|
||||
for para in list(doc.paragraphs):
|
||||
text = para.text.strip()
|
||||
if text in DIAGRAMS:
|
||||
file_name, caption = DIAGRAMS[text]
|
||||
image = PNG / file_name
|
||||
if not image.exists():
|
||||
raise FileNotFoundError(image)
|
||||
width = Inches(8.9) if "详细功能项" in text else Inches(6.7)
|
||||
add_picture_after(para, image, caption, width=width)
|
||||
inserted += 1
|
||||
if inserted != len(DIAGRAMS):
|
||||
raise RuntimeError(f"Inserted {inserted}, expected {len(DIAGRAMS)}")
|
||||
doc.save(OUT)
|
||||
print(OUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 511 KiB |
|
After Width: | Height: | Size: 433 KiB |
121
outputs/北京公交票务评估/svg设计图/01-总体技术架构图.svg
Normal file
@ -0,0 +1,121 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="960" viewBox="0 0 1440 960">
|
||||
<defs>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#1F2933" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#D71920"/>
|
||||
</marker>
|
||||
<marker id="arrow-gray" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#6F7378"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 34px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #0B2545; }
|
||||
.subtitle { font: 400 17px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.h { font: 700 20px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #0B2545; }
|
||||
.t { font: 400 15px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #1F2933; }
|
||||
.s { font: 400 13px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.tiny { font: 400 12px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.card { fill: white; stroke: #D8DEE6; stroke-width: 1.2; filter: url(#shadow); }
|
||||
.band { fill: #F5F7FA; stroke: #D8DEE6; stroke-width: 1; }
|
||||
.redline { stroke: #D71920; stroke-width: 3; fill: none; marker-end: url(#arrow-red); }
|
||||
.grayline { stroke: #6F7378; stroke-width: 2.2; fill: none; marker-end: url(#arrow-gray); }
|
||||
.dash { stroke: #6F7378; stroke-width: 2; stroke-dasharray: 8 8; fill: none; marker-end: url(#arrow-gray); }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
<path d="M0 0 H1440 V10 H0 Z" fill="#D71920"/>
|
||||
<path d="M0 10 H1440 V15 H0 Z" fill="#6F7378" opacity="0.55"/>
|
||||
<text x="64" y="72" class="title">总体技术架构图</text>
|
||||
<text x="64" y="102" class="subtitle">从用户访问、应用服务、数据存储、外部集成到运维保障的分层架构</text>
|
||||
|
||||
<rect x="58" y="138" width="1324" height="680" rx="18" class="band"/>
|
||||
<text x="92" y="178" class="h">访问与用户层</text>
|
||||
<rect x="88" y="206" width="248" height="138" rx="10" class="card"/>
|
||||
<rect x="88" y="206" width="248" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="110" y="248" class="h">集团用户</text>
|
||||
<text x="110" y="278" class="t">集团管理员 / 操作员</text>
|
||||
<text x="110" y="304" class="t">全局管理、报表、审批</text>
|
||||
<rect x="376" y="206" width="248" height="138" rx="10" class="card"/>
|
||||
<rect x="376" y="206" width="248" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="398" y="248" class="h">分公司用户</text>
|
||||
<text x="398" y="278" class="t">分公司管理员 / 操作员</text>
|
||||
<text x="398" y="304" class="t">本公司业务、审核、统计</text>
|
||||
<rect x="664" y="206" width="248" height="138" rx="10" class="card"/>
|
||||
<rect x="664" y="206" width="248" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="686" y="248" class="h">票务室用户</text>
|
||||
<text x="686" y="278" class="t">票柜、票袋、票单</text>
|
||||
<text x="686" y="304" class="t">配票、结算、退票</text>
|
||||
<rect x="952" y="206" width="248" height="138" rx="10" class="card"/>
|
||||
<rect x="952" y="206" width="248" height="8" rx="4" fill="#F2C94C"/>
|
||||
<text x="974" y="248" class="h">移动/内网终端</text>
|
||||
<text x="974" y="278" class="t">浏览器访问</text>
|
||||
<text x="974" y="304" class="t">打印、导入、导出</text>
|
||||
|
||||
<path d="M212 356 V410" class="redline"/>
|
||||
<path d="M500 356 V410" class="redline"/>
|
||||
<path d="M788 356 V410" class="redline"/>
|
||||
<path d="M1076 356 V410" class="redline"/>
|
||||
|
||||
<text x="92" y="438" class="h">应用与业务服务层</text>
|
||||
<rect x="88" y="466" width="248" height="168" rx="10" class="card"/>
|
||||
<rect x="88" y="466" width="248" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="110" y="508" class="h">前端应用</text>
|
||||
<text x="110" y="538" class="t">Vue3 / TypeScript</text>
|
||||
<text x="110" y="564" class="t">菜单导航、表单、报表</text>
|
||||
<text x="110" y="590" class="t">打印与导入导出</text>
|
||||
<rect x="376" y="466" width="248" height="168" rx="10" class="card"/>
|
||||
<rect x="376" y="466" width="248" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="398" y="508" class="h">网关与认证</text>
|
||||
<text x="398" y="538" class="t">统一登录认证</text>
|
||||
<text x="398" y="564" class="t">菜单 / 按钮权限</text>
|
||||
<text x="398" y="590" class="t">数据范围拦截</text>
|
||||
<rect x="664" y="466" width="248" height="168" rx="10" class="card"/>
|
||||
<rect x="664" y="466" width="248" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="686" y="508" class="h">业务服务</text>
|
||||
<text x="686" y="538" class="t">票库、票柜、票单</text>
|
||||
<text x="686" y="564" class="t">充值、无人售、退票</text>
|
||||
<text x="686" y="590" class="t">审批与状态机</text>
|
||||
<rect x="952" y="466" width="248" height="168" rx="10" class="card"/>
|
||||
<rect x="952" y="466" width="248" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="974" y="508" class="h">报表服务</text>
|
||||
<text x="974" y="538" class="t">日结、月结、趋势</text>
|
||||
<text x="974" y="564" class="t">Excel 导出</text>
|
||||
<text x="974" y="590" class="t">汇总表加速</text>
|
||||
|
||||
<path d="M336 550 H370" class="grayline"/>
|
||||
<path d="M624 550 H658" class="grayline"/>
|
||||
<path d="M912 550 H946" class="grayline"/>
|
||||
<path d="M788 646 V698" class="redline"/>
|
||||
|
||||
<text x="92" y="724" class="h">数据、集成与运维层</text>
|
||||
<rect x="88" y="752" width="220" height="120" rx="10" class="card"/>
|
||||
<rect x="88" y="752" width="220" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="110" y="794" class="h">达梦数据库</text>
|
||||
<text x="110" y="824" class="t">主业务库 / 主备</text>
|
||||
<text x="110" y="850" class="t">库存、流水、报表</text>
|
||||
<rect x="342" y="752" width="220" height="120" rx="10" class="card"/>
|
||||
<rect x="342" y="752" width="220" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="364" y="794" class="h">Redis / 缓存</text>
|
||||
<text x="364" y="824" class="t">会话、字典、短期任务</text>
|
||||
<text x="364" y="850" class="t">热点查询缓存</text>
|
||||
<rect x="596" y="752" width="220" height="120" rx="10" class="card"/>
|
||||
<rect x="596" y="752" width="220" height="8" rx="4" fill="#F2C94C"/>
|
||||
<text x="618" y="794" class="h">文件与对象存储</text>
|
||||
<text x="618" y="824" class="t">模板、导入文件</text>
|
||||
<text x="618" y="850" class="t">导出报表、附件</text>
|
||||
<rect x="850" y="752" width="220" height="120" rx="10" class="card"/>
|
||||
<rect x="850" y="752" width="220" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="872" y="794" class="h">外部系统集成</text>
|
||||
<text x="872" y="824" class="t">IC卡 / 数据湖 / 胆款</text>
|
||||
<text x="872" y="850" class="t">银行清分线下导入</text>
|
||||
<rect x="1104" y="752" width="220" height="120" rx="10" class="card"/>
|
||||
<rect x="1104" y="752" width="220" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="1126" y="794" class="h">运维保障</text>
|
||||
<text x="1126" y="824" class="t">日志、监控、备份</text>
|
||||
<text x="1126" y="850" class="t">灰度、回滚、巡检</text>
|
||||
|
||||
<text x="64" y="925" class="tiny">北京公交集团票务综合管理平台系统升级 | Vibe Coding 实施方案配套设计图</text>
|
||||
<circle cx="1358" cy="904" r="18" fill="#D71920" opacity="0.95"/>
|
||||
<circle cx="1395" cy="904" r="18" fill="#6F7378" opacity="0.78"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
126
outputs/北京公交票务评估/svg设计图/02-核心票务业务流转图.svg
Normal file
@ -0,0 +1,126 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="960" viewBox="0 0 1440 960">
|
||||
<defs>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#1F2933" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#D71920"/>
|
||||
</marker>
|
||||
<marker id="arrow-gray" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#6F7378"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 34px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #0B2545; }
|
||||
.subtitle { font: 400 17px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.h { font: 700 20px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #0B2545; }
|
||||
.t { font: 400 15px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #1F2933; }
|
||||
.s { font: 400 13px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.tiny { font: 400 12px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.card { fill: white; stroke: #D8DEE6; stroke-width: 1.2; filter: url(#shadow); }
|
||||
.band { fill: #F5F7FA; stroke: #D8DEE6; stroke-width: 1; }
|
||||
.redline { stroke: #D71920; stroke-width: 3; fill: none; marker-end: url(#arrow-red); }
|
||||
.grayline { stroke: #6F7378; stroke-width: 2.2; fill: none; marker-end: url(#arrow-gray); }
|
||||
.dash { stroke: #6F7378; stroke-width: 2; stroke-dasharray: 8 8; fill: none; marker-end: url(#arrow-gray); }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
<path d="M0 0 H1440 V10 H0 Z" fill="#D71920"/>
|
||||
<path d="M0 10 H1440 V15 H0 Z" fill="#6F7378" opacity="0.55"/>
|
||||
<text x="64" y="72" class="title">核心票务业务流转图</text>
|
||||
<text x="64" y="102" class="subtitle">围绕票库、票柜、票袋、票单、结算、退票和报表的主业务闭环</text>
|
||||
|
||||
<rect x="74" y="150" width="1290" height="700" rx="18" class="band"/>
|
||||
<rect x="104" y="190" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="104" y="190" width="220" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="126" y="232" class="h">1. 印制入库</text>
|
||||
<text x="126" y="262" class="t">分公司发起印制申请</text>
|
||||
<text x="126" y="288" class="t">集团审核</text>
|
||||
<text x="126" y="314" class="t">确认后进入票库</text>
|
||||
<rect x="384" y="190" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="384" y="190" width="220" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="406" y="232" class="h">2. 票库管理</text>
|
||||
<text x="406" y="262" class="t">票号段入库</text>
|
||||
<text x="406" y="288" class="t">库存余额与流水</text>
|
||||
<text x="406" y="314" class="t">调拨 / 核销 / 调账</text>
|
||||
<rect x="664" y="190" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="664" y="190" width="220" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="686" y="232" class="h">3. 配票申请</text>
|
||||
<text x="686" y="262" class="t">票务室申请</text>
|
||||
<text x="686" y="288" class="t">分公司确认</text>
|
||||
<text x="686" y="314" class="t">票库到票柜</text>
|
||||
<rect x="944" y="190" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="944" y="190" width="220" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="966" y="232" class="h">4. 票柜库存</text>
|
||||
<text x="966" y="262" class="t">票务室库存</text>
|
||||
<text x="966" y="288" class="t">入库 / 出库查询</text>
|
||||
<text x="966" y="314" class="t">票务室调票</text>
|
||||
|
||||
<path d="M324 253 H378" class="redline"/>
|
||||
<path d="M604 253 H658" class="redline"/>
|
||||
<path d="M884 253 H938" class="redline"/>
|
||||
|
||||
<rect x="104" y="400" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="104" y="400" width="220" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="126" y="442" class="h">5. 票袋管理</text>
|
||||
<text x="126" y="472" class="t">绑定线路、售票员</text>
|
||||
<text x="126" y="498" class="t">更换线路</text>
|
||||
<text x="126" y="524" class="t">票袋库存归属</text>
|
||||
<rect x="384" y="400" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="384" y="400" width="220" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="406" y="442" class="h">6. 票单配票</text>
|
||||
<text x="406" y="472" class="t">加载票袋数据</text>
|
||||
<text x="406" y="498" class="t">录入起号止号</text>
|
||||
<text x="406" y="524" class="t">票柜出库到票袋</text>
|
||||
<rect x="664" y="400" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="664" y="400" width="220" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="686" y="442" class="h">7. 票单结算</text>
|
||||
<text x="686" y="472" class="t">录入剩余票号</text>
|
||||
<text x="686" y="498" class="t">自动计算张数金额</text>
|
||||
<text x="686" y="524" class="t">生成结算记录</text>
|
||||
<rect x="944" y="400" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="944" y="400" width="220" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="966" y="442" class="h">8. 票单查询</text>
|
||||
<text x="966" y="472" class="t">配票单 / 结算单</text>
|
||||
<text x="966" y="498" class="t">打印、导出</text>
|
||||
<text x="966" y="524" class="t">重新结算入口</text>
|
||||
|
||||
<path d="M1054 328 C1054 362 260 362 214 394" class="grayline"/>
|
||||
<path d="M324 463 H378" class="redline"/>
|
||||
<path d="M604 463 H658" class="redline"/>
|
||||
<path d="M884 463 H938" class="redline"/>
|
||||
|
||||
<rect x="104" y="610" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="104" y="610" width="220" height="8" rx="4" fill="#B63A2E"/>
|
||||
<text x="126" y="652" class="h">9. 退票处理</text>
|
||||
<text x="126" y="682" class="t">票袋退票</text>
|
||||
<text x="126" y="708" class="t">票柜退票</text>
|
||||
<text x="126" y="734" class="t">库存回写</text>
|
||||
<rect x="384" y="610" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="384" y="610" width="220" height="8" rx="4" fill="#F2C94C"/>
|
||||
<text x="406" y="652" class="h">10. 无人售/充值</text>
|
||||
<text x="406" y="682" class="t">无人售线路结算</text>
|
||||
<text x="406" y="708" class="t">IC卡库存与收入</text>
|
||||
<text x="406" y="734" class="t">导入与异常处理</text>
|
||||
<rect x="664" y="610" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="664" y="610" width="220" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="686" y="652" class="h">11. 报表汇总</text>
|
||||
<text x="686" y="682" class="t">日结、月结</text>
|
||||
<text x="686" y="708" class="t">收入、清分、趋势</text>
|
||||
<text x="686" y="734" class="t">三级数据权限</text>
|
||||
<rect x="944" y="610" width="220" height="126" rx="10" class="card"/>
|
||||
<rect x="944" y="610" width="220" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="966" y="652" class="h">12. 审计追溯</text>
|
||||
<text x="966" y="682" class="t">库存流水</text>
|
||||
<text x="966" y="708" class="t">操作日志</text>
|
||||
<text x="966" y="734" class="t">票号查询</text>
|
||||
|
||||
<path d="M214 538 V604" class="grayline"/>
|
||||
<path d="M774 538 V604" class="redline"/>
|
||||
<path d="M884 673 H938" class="grayline"/>
|
||||
<path d="M1054 598 C1054 568 1054 560 1054 538" class="dash"/>
|
||||
<text x="1178" y="557" class="s">异常更正 / 重新结算</text>
|
||||
|
||||
<text x="64" y="925" class="tiny">北京公交集团票务综合管理平台系统升级 | Vibe Coding 实施方案配套设计图</text>
|
||||
<circle cx="1358" cy="904" r="18" fill="#D71920" opacity="0.95"/>
|
||||
<circle cx="1395" cy="904" r="18" fill="#6F7378" opacity="0.78"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
116
outputs/北京公交票务评估/svg设计图/03-部署与可运维性架构图.svg
Normal file
@ -0,0 +1,116 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="960" viewBox="0 0 1440 960">
|
||||
<defs>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#1F2933" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#D71920"/>
|
||||
</marker>
|
||||
<marker id="arrow-gray" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#6F7378"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 34px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #0B2545; }
|
||||
.subtitle { font: 400 17px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.h { font: 700 20px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #0B2545; }
|
||||
.t { font: 400 15px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #1F2933; }
|
||||
.s { font: 400 13px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.tiny { font: 400 12px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.card { fill: white; stroke: #D8DEE6; stroke-width: 1.2; filter: url(#shadow); }
|
||||
.band { fill: #F5F7FA; stroke: #D8DEE6; stroke-width: 1; }
|
||||
.redline { stroke: #D71920; stroke-width: 3; fill: none; marker-end: url(#arrow-red); }
|
||||
.grayline { stroke: #6F7378; stroke-width: 2.2; fill: none; marker-end: url(#arrow-gray); }
|
||||
.dash { stroke: #6F7378; stroke-width: 2; stroke-dasharray: 8 8; fill: none; marker-end: url(#arrow-gray); }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
<path d="M0 0 H1440 V10 H0 Z" fill="#D71920"/>
|
||||
<path d="M0 10 H1440 V15 H0 Z" fill="#6F7378" opacity="0.55"/>
|
||||
<text x="64" y="72" class="title">部署与可运维性架构图</text>
|
||||
<text x="64" y="102" class="subtitle">面向国产化环境的部署、监控、备份、回滚和运维交接设计</text>
|
||||
|
||||
<rect x="70" y="148" width="1300" height="704" rx="18" class="band"/>
|
||||
<text x="102" y="188" class="h">网络与访问区</text>
|
||||
<rect x="104" y="214" width="230" height="116" rx="10" class="card"/>
|
||||
<rect x="104" y="214" width="230" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="126" y="256" class="h">用户浏览器</text>
|
||||
<text x="126" y="286" class="t">集团 / 分公司 / 票务室</text>
|
||||
<text x="126" y="312" class="t">内网访问、统一认证</text>
|
||||
<rect x="394" y="214" width="230" height="116" rx="10" class="card"/>
|
||||
<rect x="394" y="214" width="230" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="416" y="256" class="h">Nginx / 网关</text>
|
||||
<text x="416" y="286" class="t">HTTPS 终止</text>
|
||||
<text x="416" y="312" class="t">反向代理、限流</text>
|
||||
<rect x="684" y="214" width="230" height="116" rx="10" class="card"/>
|
||||
<rect x="684" y="214" width="230" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="706" y="256" class="h">应用服务 A</text>
|
||||
<text x="706" y="286" class="t">业务 API</text>
|
||||
<text x="706" y="312" class="t">前端静态资源</text>
|
||||
<rect x="974" y="214" width="230" height="116" rx="10" class="card"/>
|
||||
<rect x="974" y="214" width="230" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="996" y="256" class="h">应用服务 B</text>
|
||||
<text x="996" y="286" class="t">热备 / 横向扩展</text>
|
||||
<text x="996" y="312" class="t">版本灰度</text>
|
||||
<path d="M334 272 H388" class="redline"/>
|
||||
<path d="M624 272 H678" class="redline"/>
|
||||
<path d="M914 272 H968" class="grayline"/>
|
||||
|
||||
<text x="102" y="398" class="h">数据与文件区</text>
|
||||
<rect x="104" y="426" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="104" y="426" width="230" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="126" y="468" class="h">达梦主库</text>
|
||||
<text x="126" y="498" class="t">业务数据写入</text>
|
||||
<text x="126" y="524" class="t">库存与单据主数据</text>
|
||||
<rect x="394" y="426" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="394" y="426" width="230" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="416" y="468" class="h">达梦备库</text>
|
||||
<text x="416" y="498" class="t">主备同步</text>
|
||||
<text x="416" y="524" class="t">故障切换准备</text>
|
||||
<rect x="684" y="426" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="684" y="426" width="230" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="706" y="468" class="h">Redis</text>
|
||||
<text x="706" y="498" class="t">会话、缓存</text>
|
||||
<text x="706" y="524" class="t">任务状态、热点字典</text>
|
||||
<rect x="974" y="426" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="974" y="426" width="230" height="8" rx="4" fill="#F2C94C"/>
|
||||
<text x="996" y="468" class="h">对象存储</text>
|
||||
<text x="996" y="498" class="t">模板、导入文件</text>
|
||||
<text x="996" y="524" class="t">导出报表、附件</text>
|
||||
<path d="M334 490 H388" class="grayline"/>
|
||||
<path d="M799 342 V420" class="redline"/>
|
||||
<path d="M1089 342 V420" class="grayline"/>
|
||||
|
||||
<text x="102" y="620" class="h">运维保障区</text>
|
||||
<rect x="104" y="648" width="216" height="124" rx="10" class="card"/>
|
||||
<rect x="104" y="648" width="216" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="126" y="690" class="h">日志中心</text>
|
||||
<text x="126" y="720" class="t">业务日志、接口日志</text>
|
||||
<text x="126" y="746" class="t">登录与操作审计</text>
|
||||
<rect x="360" y="648" width="216" height="124" rx="10" class="card"/>
|
||||
<rect x="360" y="648" width="216" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="382" y="690" class="h">监控告警</text>
|
||||
<text x="382" y="720" class="t">存活、慢SQL、磁盘</text>
|
||||
<text x="382" y="746" class="t">异常率、任务失败</text>
|
||||
<rect x="616" y="648" width="216" height="124" rx="10" class="card"/>
|
||||
<rect x="616" y="648" width="216" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="638" y="690" class="h">备份恢复</text>
|
||||
<text x="638" y="720" class="t">数据库全量/增量</text>
|
||||
<text x="638" y="746" class="t">文件备份、恢复演练</text>
|
||||
<rect x="872" y="648" width="216" height="124" rx="10" class="card"/>
|
||||
<rect x="872" y="648" width="216" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="894" y="690" class="h">CI/CD</text>
|
||||
<text x="894" y="720" class="t">构建、测试、制品</text>
|
||||
<text x="894" y="746" class="t">部署、版本标记</text>
|
||||
<rect x="1128" y="648" width="216" height="124" rx="10" class="card"/>
|
||||
<rect x="1128" y="648" width="216" height="8" rx="4" fill="#B63A2E"/>
|
||||
<text x="1150" y="690" class="h">回滚预案</text>
|
||||
<text x="1150" y="720" class="t">旧版本保留</text>
|
||||
<text x="1150" y="746" class="t">割接窗口、冒烟检查</text>
|
||||
<path d="M684 592 C550 610 465 620 468 642" class="dash"/>
|
||||
<path d="M914 592 C790 610 724 622 724 642" class="dash"/>
|
||||
<path d="M624 272 C520 340 475 380 480 420" class="dash"/>
|
||||
|
||||
<text x="64" y="925" class="tiny">北京公交集团票务综合管理平台系统升级 | Vibe Coding 实施方案配套设计图</text>
|
||||
<circle cx="1358" cy="904" r="18" fill="#D71920" opacity="0.95"/>
|
||||
<circle cx="1395" cy="904" r="18" fill="#6F7378" opacity="0.78"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
97
outputs/北京公交票务评估/svg设计图/04-权限与数据范围设计图.svg
Normal file
@ -0,0 +1,97 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="960" viewBox="0 0 1440 960">
|
||||
<defs>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#1F2933" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#D71920"/>
|
||||
</marker>
|
||||
<marker id="arrow-gray" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#6F7378"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 34px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #0B2545; }
|
||||
.subtitle { font: 400 17px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.h { font: 700 20px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #0B2545; }
|
||||
.t { font: 400 15px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #1F2933; }
|
||||
.s { font: 400 13px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.tiny { font: 400 12px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.card { fill: white; stroke: #D8DEE6; stroke-width: 1.2; filter: url(#shadow); }
|
||||
.band { fill: #F5F7FA; stroke: #D8DEE6; stroke-width: 1; }
|
||||
.redline { stroke: #D71920; stroke-width: 3; fill: none; marker-end: url(#arrow-red); }
|
||||
.grayline { stroke: #6F7378; stroke-width: 2.2; fill: none; marker-end: url(#arrow-gray); }
|
||||
.dash { stroke: #6F7378; stroke-width: 2; stroke-dasharray: 8 8; fill: none; marker-end: url(#arrow-gray); }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
<path d="M0 0 H1440 V10 H0 Z" fill="#D71920"/>
|
||||
<path d="M0 10 H1440 V15 H0 Z" fill="#6F7378" opacity="0.55"/>
|
||||
<text x="64" y="72" class="title">权限与数据范围设计图</text>
|
||||
<text x="64" y="102" class="subtitle">菜单权限、按钮权限、接口权限、数据权限和导出权限的统一控制</text>
|
||||
|
||||
<rect x="70" y="148" width="1300" height="704" rx="18" class="band"/>
|
||||
<rect x="96" y="198" width="240" height="134" rx="10" class="card"/>
|
||||
<rect x="96" y="198" width="240" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="118" y="240" class="h">集团管理员</text>
|
||||
<text x="118" y="270" class="t">配置菜单、角色、用户</text>
|
||||
<text x="118" y="296" class="t">分配分公司数据范围</text>
|
||||
<text x="118" y="322" class="t">查看全集团数据</text>
|
||||
<rect x="396" y="198" width="240" height="134" rx="10" class="card"/>
|
||||
<rect x="396" y="198" width="240" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="418" y="240" class="h">集团操作员</text>
|
||||
<text x="418" y="270" class="t">按角色使用功能</text>
|
||||
<text x="418" y="296" class="t">查看授权公司数据</text>
|
||||
<text x="418" y="322" class="t">集团级报表</text>
|
||||
<rect x="696" y="198" width="240" height="134" rx="10" class="card"/>
|
||||
<rect x="696" y="198" width="240" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="718" y="240" class="h">分公司管理员</text>
|
||||
<text x="718" y="270" class="t">维护本公司用户</text>
|
||||
<text x="718" y="296" class="t">配置票务室权限</text>
|
||||
<text x="718" y="322" class="t">本公司业务管理</text>
|
||||
<rect x="996" y="198" width="240" height="134" rx="10" class="card"/>
|
||||
<rect x="996" y="198" width="240" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="1018" y="240" class="h">票务室操作员</text>
|
||||
<text x="1018" y="270" class="t">票柜、票袋、票单</text>
|
||||
<text x="1018" y="296" class="t">结算、退票、导出</text>
|
||||
<text x="1018" y="322" class="t">仅本票务室数据</text>
|
||||
|
||||
<rect x="170" y="414" width="1100" height="100" rx="12" fill="#FFFFFF" stroke="#D8DEE6" filter="url(#shadow)"/>
|
||||
<text x="212" y="456" class="h">统一权限拦截层</text>
|
||||
<text x="212" y="486" class="t">菜单权限 → 按钮权限 → 接口权限 → 数据范围权限 → 导出/打印权限</text>
|
||||
<path d="M216 344 V408" class="redline"/>
|
||||
<path d="M516 344 V408" class="grayline"/>
|
||||
<path d="M816 344 V408" class="grayline"/>
|
||||
<path d="M1116 344 V408" class="grayline"/>
|
||||
|
||||
<rect x="116" y="604" width="230" height="126" rx="10" class="card"/>
|
||||
<rect x="116" y="604" width="230" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="138" y="646" class="h">组织维度</text>
|
||||
<text x="138" y="676" class="t">集团</text>
|
||||
<text x="138" y="702" class="t">分公司</text>
|
||||
<text x="138" y="728" class="t">票务室 / 车队 / 线路</text>
|
||||
<rect x="386" y="604" width="230" height="126" rx="10" class="card"/>
|
||||
<rect x="386" y="604" width="230" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="408" y="646" class="h">业务维度</text>
|
||||
<text x="408" y="676" class="t">票库 / 票柜 / 票袋</text>
|
||||
<text x="408" y="702" class="t">充值网点</text>
|
||||
<text x="408" y="728" class="t">无人售线路</text>
|
||||
<rect x="656" y="604" width="230" height="126" rx="10" class="card"/>
|
||||
<rect x="656" y="604" width="230" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="678" y="646" class="h">数据维度</text>
|
||||
<text x="678" y="676" class="t">单据、库存、流水</text>
|
||||
<text x="678" y="702" class="t">报表汇总</text>
|
||||
<text x="678" y="728" class="t">历史迁移数据</text>
|
||||
<rect x="926" y="604" width="230" height="126" rx="10" class="card"/>
|
||||
<rect x="926" y="604" width="230" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="948" y="646" class="h">审计维度</text>
|
||||
<text x="948" y="676" class="t">登录日志</text>
|
||||
<text x="948" y="702" class="t">操作日志</text>
|
||||
<text x="948" y="728" class="t">导入导出日志</text>
|
||||
<path d="M720 524 V598" class="redline"/>
|
||||
<path d="M720 524 C530 548 500 570 500 598" class="grayline"/>
|
||||
<path d="M720 524 C902 548 1040 570 1040 598" class="grayline"/>
|
||||
|
||||
<text x="64" y="925" class="tiny">北京公交集团票务综合管理平台系统升级 | Vibe Coding 实施方案配套设计图</text>
|
||||
<circle cx="1358" cy="904" r="18" fill="#D71920" opacity="0.95"/>
|
||||
<circle cx="1395" cy="904" r="18" fill="#6F7378" opacity="0.78"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
98
outputs/北京公交票务评估/svg设计图/05-VibeCoding协同交付流程图.svg
Normal file
@ -0,0 +1,98 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="960" viewBox="0 0 1440 960">
|
||||
<defs>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#1F2933" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#D71920"/>
|
||||
</marker>
|
||||
<marker id="arrow-gray" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto">
|
||||
<path d="M0,0 L10,5 L0,10 Z" fill="#6F7378"/>
|
||||
</marker>
|
||||
<style>
|
||||
.title { font: 700 34px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #0B2545; }
|
||||
.subtitle { font: 400 17px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.h { font: 700 20px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #0B2545; }
|
||||
.t { font: 400 15px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #1F2933; }
|
||||
.s { font: 400 13px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.tiny { font: 400 12px "Microsoft YaHei", "PingFang SC", Arial, sans-serif; fill: #6F7378; }
|
||||
.card { fill: white; stroke: #D8DEE6; stroke-width: 1.2; filter: url(#shadow); }
|
||||
.band { fill: #F5F7FA; stroke: #D8DEE6; stroke-width: 1; }
|
||||
.redline { stroke: #D71920; stroke-width: 3; fill: none; marker-end: url(#arrow-red); }
|
||||
.grayline { stroke: #6F7378; stroke-width: 2.2; fill: none; marker-end: url(#arrow-gray); }
|
||||
.dash { stroke: #6F7378; stroke-width: 2; stroke-dasharray: 8 8; fill: none; marker-end: url(#arrow-gray); }
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
<path d="M0 0 H1440 V10 H0 Z" fill="#D71920"/>
|
||||
<path d="M0 10 H1440 V15 H0 Z" fill="#6F7378" opacity="0.55"/>
|
||||
<text x="64" y="72" class="title">Vibe Coding 协同交付流程图</text>
|
||||
<text x="64" y="102" class="subtitle">把大系统拆成可验证的小任务,形成需求、编码、测试、评审、交付闭环</text>
|
||||
|
||||
<rect x="74" y="148" width="1290" height="704" rx="18" class="band"/>
|
||||
<rect x="102" y="202" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="102" y="202" width="230" height="8" rx="4" fill="#D71920"/>
|
||||
<text x="124" y="244" class="h">1. 需求卡片化</text>
|
||||
<text x="124" y="274" class="t">按页面/动作拆分</text>
|
||||
<text x="124" y="300" class="t">角色、输入、输出</text>
|
||||
<text x="124" y="326" class="t">验收标准</text>
|
||||
<rect x="382" y="202" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="382" y="202" width="230" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="404" y="244" class="h">2. 领域建模</text>
|
||||
<text x="404" y="274" class="t">状态机、票号段</text>
|
||||
<text x="404" y="300" class="t">库存流水、报表口径</text>
|
||||
<text x="404" y="326" class="t">人工确认</text>
|
||||
<rect x="662" y="202" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="662" y="202" width="230" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="684" y="244" class="h">3. 任务生成</text>
|
||||
<text x="684" y="274" class="t">表结构、接口</text>
|
||||
<text x="684" y="300" class="t">页面、测试</text>
|
||||
<text x="684" y="326" class="t">小批量执行</text>
|
||||
<rect x="942" y="202" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="942" y="202" width="230" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="964" y="244" class="h">4. 编码实现</text>
|
||||
<text x="964" y="274" class="t">前后端协同</text>
|
||||
<text x="964" y="300" class="t">导入导出</text>
|
||||
<text x="964" y="326" class="t">权限与日志</text>
|
||||
<path d="M332 266 H376" class="redline"/>
|
||||
<path d="M612 266 H656" class="redline"/>
|
||||
<path d="M892 266 H936" class="redline"/>
|
||||
|
||||
<rect x="102" y="470" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="102" y="470" width="230" height="8" rx="4" fill="#B63A2E"/>
|
||||
<text x="124" y="512" class="h">8. 模块验收</text>
|
||||
<text x="124" y="542" class="t">业务演示</text>
|
||||
<text x="124" y="568" class="t">样例数据核对</text>
|
||||
<text x="124" y="594" class="t">问题闭环</text>
|
||||
<rect x="382" y="470" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="382" y="470" width="230" height="8" rx="4" fill="#6F7378"/>
|
||||
<text x="404" y="512" class="h">7. 人工评审</text>
|
||||
<text x="404" y="542" class="t">核心规则 review</text>
|
||||
<text x="404" y="568" class="t">安全与权限检查</text>
|
||||
<text x="404" y="594" class="t">SQL 与事务检查</text>
|
||||
<rect x="662" y="470" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="662" y="470" width="230" height="8" rx="4" fill="#2E86C1"/>
|
||||
<text x="684" y="512" class="h">6. 自动化测试</text>
|
||||
<text x="684" y="542" class="t">单元 / 接口</text>
|
||||
<text x="684" y="568" class="t">场景 / 回归</text>
|
||||
<text x="684" y="594" class="t">构建检查</text>
|
||||
<rect x="942" y="470" width="230" height="128" rx="10" class="card"/>
|
||||
<rect x="942" y="470" width="230" height="8" rx="4" fill="#3FA45B"/>
|
||||
<text x="964" y="512" class="h">5. 本地自测</text>
|
||||
<text x="964" y="542" class="t">页面联调</text>
|
||||
<text x="964" y="568" class="t">异常分支</text>
|
||||
<text x="964" y="594" class="t">导入导出校验</text>
|
||||
<path d="M1057 342 V464" class="redline"/>
|
||||
<path d="M942 534 H898" class="grayline"/>
|
||||
<path d="M662 534 H618" class="grayline"/>
|
||||
<path d="M382 534 H338" class="grayline"/>
|
||||
|
||||
<rect x="208" y="690" width="944" height="74" rx="12" fill="#FFFFFF" stroke="#D8DEE6" filter="url(#shadow)"/>
|
||||
<text x="238" y="726" class="h">交付原则</text>
|
||||
<text x="362" y="724" class="t">小任务、强测试、人工把关、持续演示;常规功能提效,核心规则不省评审。</text>
|
||||
<path d="M217 608 C217 670 678 650 678 684" class="dash"/>
|
||||
|
||||
<text x="64" y="925" class="tiny">北京公交集团票务综合管理平台系统升级 | Vibe Coding 实施方案配套设计图</text>
|
||||
<circle cx="1358" cy="904" r="18" fill="#D71920" opacity="0.95"/>
|
||||
<circle cx="1395" cy="904" r="18" fill="#6F7378" opacity="0.78"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
7
outputs/北京公交票务评估/svg设计图/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# SVG 设计图清单
|
||||
|
||||
- 01-总体技术架构图.svg
|
||||
- 02-核心票务业务流转图.svg
|
||||
- 03-部署与可运维性架构图.svg
|
||||
- 04-权限与数据范围设计图.svg
|
||||
- 05-VibeCoding协同交付流程图.svg
|
||||
BIN
outputs/北京公交票务评估/svg设计图/png预览/01-总体技术架构图.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
outputs/北京公交票务评估/svg设计图/png预览/02-核心票务业务流转图.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
outputs/北京公交票务评估/svg设计图/png预览/03-部署与可运维性架构图.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
outputs/北京公交票务评估/svg设计图/png预览/04-权限与数据范围设计图.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
outputs/北京公交票务评估/svg设计图/png预览/05-VibeCoding协同交付流程图.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
outputs/北京公交票务评估/svg设计图/quicklook/01-总体技术架构图.svg.png
Normal file
|
After Width: | Height: | Size: 279 KiB |