feat(workflow,trigger): async dispatch + GC schedulers + trigger list polish

This commit is contained in:
matevip 2026-05-08 15:07:55 +08:00
parent 533868c41b
commit 3ed540e6eb
28 changed files with 2056 additions and 17 deletions

View File

@ -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.). */

View File

@ -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 &lt;
* 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); }

View File

@ -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

View File

@ -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.',

View File

@ -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 进一步过滤。',

View File

@ -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;

View 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()

View 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)

View 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()

View 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()

View 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

View 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

View 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

View 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

View 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

View File

@ -0,0 +1,7 @@
# SVG 设计图清单
- 01-总体技术架构图.svg
- 02-核心票务业务流转图.svg
- 03-部署与可运维性架构图.svg
- 04-权限与数据范围设计图.svg
- 05-VibeCoding协同交付流程图.svg

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB