Эх сурвалжийг харах

fix(s0): add breakdown table sync flag and pi config endpoint for contract review

YY968XX 2 өдөр өмнө
parent
commit
36570fa841

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.180",
+	"version": "2.4.181",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 74 - 0
Web/src/views/aidop/s0/api/s0SalesApi.ts

@@ -424,6 +424,80 @@ export const s0ContractReviewCyclesApi = {
 		service.patch<S0ContractReviewCycleRow>(`/api/s0/sales/contract-review-cycles/${id}/toggle-enabled`, body).then(unwrap),
 };
 
+// ── B1.2: 合同评审周期同步开关(工厂级一行) ──────────────────────────
+
+export interface S0ContractReviewCycleSyncFlag {
+	id?: number | null;
+	companyRefId?: string | null;
+	factoryRefId: string;
+	domainCode?: string | null;
+	isSyncEnabled: boolean;
+}
+
+export interface S0ContractReviewCycleSyncFlagUpsert {
+	companyRefId: string;
+	factoryRefId: string;
+	domainCode?: string;
+	isSyncEnabled: boolean;
+	createUser?: string;
+	updateUser?: string;
+}
+
+export const s0ContractReviewCycleSyncFlagApi = {
+	get: (params: { factoryRefId: string }) =>
+		service.get<S0ContractReviewCycleSyncFlag>('/api/s0/sales/contract-review-cycle-sync-flag', { params }).then(unwrap),
+	update: (body: S0ContractReviewCycleSyncFlagUpsert) =>
+		service.put<S0ContractReviewCycleSyncFlag>('/api/s0/sales/contract-review-cycle-sync-flag', body).then(unwrap),
+};
+
+// ── B1.3: 合同评审周期下钻配置(按 parent_stage_code 分组) ──────────
+
+export interface S0ContractReviewCycleBreakdownRow {
+	id: number;
+	companyRefId?: string;
+	factoryRefId?: string;
+	domainCode?: string | null;
+	parentStageCode: string;
+	groupCode: string;
+	groupName: string;
+	stdHours: number;
+	orderNo: number;
+	isActive: boolean;
+	createUser?: string | null;
+	createTime?: string;
+	updateUser?: string | null;
+	updateTime?: string | null;
+}
+
+export interface S0ContractReviewCycleBreakdownUpsert {
+	companyRefId?: string;
+	factoryRefId?: string;
+	domainCode?: string;
+	parentStageCode: string;
+	groupCode: string;
+	groupName: string;
+	stdHours: number;
+	orderNo: number;
+	isActive: boolean;
+	createUser?: string;
+	updateUser?: string;
+}
+
+export const s0ContractReviewCycleBreakdownsApi = {
+	list: (params: Record<string, unknown>) =>
+		service.get<Paged<S0ContractReviewCycleBreakdownRow>>('/api/s0/sales/contract-review-cycle-breakdowns', { params }).then(unwrap),
+	get: (id: number) =>
+		service.get<S0ContractReviewCycleBreakdownRow>(`/api/s0/sales/contract-review-cycle-breakdowns/${id}`).then(unwrap),
+	create: (body: S0ContractReviewCycleBreakdownUpsert) =>
+		service.post<S0ContractReviewCycleBreakdownRow>('/api/s0/sales/contract-review-cycle-breakdowns', body).then(unwrap),
+	update: (id: number, body: S0ContractReviewCycleBreakdownUpsert) =>
+		service.put<S0ContractReviewCycleBreakdownRow>(`/api/s0/sales/contract-review-cycle-breakdowns/${id}`, body).then(unwrap),
+	delete: (id: number) =>
+		service.delete(`/api/s0/sales/contract-review-cycle-breakdowns/${id}`).then(unwrap),
+	toggleEnabled: (id: number, body: { isActive: boolean }) =>
+		service.patch<S0ContractReviewCycleBreakdownRow>(`/api/s0/sales/contract-review-cycle-breakdowns/${id}/toggle-enabled`, body).then(unwrap),
+};
+
 // ── B2: 产品设计周期标准 ──────────────────────────────────────────────
 
 export interface S0ProductDesignCycleRow {

+ 369 - 0
Web/src/views/aidop/s0/sales/ContractReviewCycleList.vue

@@ -1,5 +1,17 @@
 <template>
 	<AidopDemoShell :title="pageTitle" subtitle="S0 / Sales / 合同评审周期">
+		<!-- ── 同步当前配置开关(工厂级) ─────────────────────────────────── -->
+		<div class="sync-bar">
+			<span class="sync-bar__label">同步当前配置:</span>
+			<el-switch
+				v-model="syncEnabled"
+				:disabled="syncDisabled"
+				:loading="syncLoading"
+				@change="onSyncChange"
+			/>
+			<span class="sync-bar__text">{{ syncBarText }}</span>
+		</div>
+
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
 			<el-form-item label="公司">
 				<el-select v-model="query.companyRefId" clearable filterable placeholder="全部" style="width: 180px">
@@ -62,6 +74,51 @@
 			/>
 		</div>
 
+		<!-- ── 意见反馈下钻配置 ──────────────────────────────────────── -->
+		<section class="breakdown-section">
+			<div class="breakdown-section__head">
+				<h3 class="breakdown-section__title">意见反馈 — 部门/组 PI 配置</h3>
+				<div class="breakdown-section__actions">
+					<el-button :disabled="!query.factoryRefId" @click="loadBreakdown">刷新</el-button>
+					<el-button type="success" :disabled="!query.factoryRefId" @click="openBreakdownCreate">新增下钻</el-button>
+				</div>
+			</div>
+
+			<div v-if="!query.factoryRefId" class="breakdown-section__hint">
+				请先选择工厂后维护意见反馈下钻配置
+			</div>
+			<el-table
+				v-else
+				:data="breakdownRows"
+				v-loading="breakdownLoading"
+				border
+				stripe
+				size="small"
+				style="width: 100%"
+			>
+				<el-table-column prop="groupCode" label="组编码" width="160" class-name="breakdown-section__wrap-cell" />
+				<el-table-column prop="groupName" label="组名称" min-width="160" class-name="breakdown-section__wrap-cell" />
+				<el-table-column prop="stdHours" label="标准时长(h)" width="130" align="right" />
+				<el-table-column prop="orderNo" label="顺序" width="80" align="center" />
+				<el-table-column label="是否启用" width="100" align="center">
+					<template #default="{ row }">
+						<el-tag :type="row.isActive ? 'success' : 'info'" size="small">
+							{{ row.isActive ? '是' : '否' }}
+						</el-tag>
+					</template>
+				</el-table-column>
+				<el-table-column label="操作" width="220" fixed="right" align="center">
+					<template #default="{ row }">
+						<el-button link type="primary" @click="openBreakdownEdit(row)">编辑</el-button>
+						<el-button link :type="row.isActive ? 'warning' : 'success'" @click="breakdownToggleActive(row)">
+							{{ row.isActive ? '禁用' : '启用' }}
+						</el-button>
+						<el-button link type="danger" @click="onBreakdownDelete(row)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+		</section>
+
 		<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" destroy-on-close @closed="resetForm">
 			<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
 				<el-row :gutter="16">
@@ -117,6 +174,40 @@
 				<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
 			</template>
 		</el-dialog>
+
+		<!-- ── 下钻 新增/编辑 弹窗 ──────────────────────────────────── -->
+		<el-dialog
+			v-model="breakdownDialogVisible"
+			:title="breakdownDialogTitle"
+			width="560px"
+			destroy-on-close
+			@closed="resetBreakdownForm"
+		>
+			<el-form ref="breakdownFormRef" :model="breakdownForm" :rules="breakdownRules" label-width="110px">
+				<el-form-item label="父阶段">
+					<el-input :model-value="STAGE_NAME_MAP[BREAKDOWN_PARENT_STAGE_CODE]" disabled />
+				</el-form-item>
+				<el-form-item label="组编码" prop="groupCode">
+					<el-input v-model="breakdownForm.groupCode" placeholder="例如:market" />
+				</el-form-item>
+				<el-form-item label="组名称" prop="groupName">
+					<el-input v-model="breakdownForm.groupName" placeholder="例如:市场部" />
+				</el-form-item>
+				<el-form-item label="标准时长(h)" prop="stdHours">
+					<el-input-number v-model="breakdownForm.stdHours" :min="0" :step="0.1" :precision="2" style="width: 100%" />
+				</el-form-item>
+				<el-form-item label="顺序">
+					<el-input-number v-model="breakdownForm.orderNo" :min="1" style="width: 100%" />
+				</el-form-item>
+				<el-form-item label="是否启用">
+					<el-switch v-model="breakdownForm.isActive" />
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<el-button @click="breakdownDialogVisible = false">取消</el-button>
+				<el-button type="primary" :loading="breakdownSaving" @click="submitBreakdownForm">保存</el-button>
+			</template>
+		</el-dialog>
 	</AidopDemoShell>
 </template>
 
@@ -128,9 +219,13 @@ import AidopDemoShell from '../../components/AidopDemoShell.vue';
 import {
 	loadOrgList,
 	s0ContractReviewCyclesApi,
+	s0ContractReviewCycleSyncFlagApi,
+	s0ContractReviewCycleBreakdownsApi,
 	type OrgOption,
 	type S0ContractReviewCycleRow,
 	type S0ContractReviewCycleUpsert,
+	type S0ContractReviewCycleBreakdownRow,
+	type S0ContractReviewCycleBreakdownUpsert,
 } from '../api/s0SalesApi';
 
 const route = useRoute();
@@ -165,6 +260,25 @@ const formRef = ref<FormInstance>();
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 
+// ── 同步开关(工厂级) ─────────────────────────────────────────────────
+const syncEnabled = ref(false);
+const syncLoading = ref(false);
+const syncDisabled = computed(() => !query.factoryRefId);
+const syncBarText = computed(() => {
+	if (!query.factoryRefId) return '请先选择工厂';
+	return syncEnabled.value ? '当前工厂配置已同步到 S8' : '当前工厂未同步';
+});
+
+// ── 意见反馈下钻配置 ───────────────────────────────────────────────────
+const BREAKDOWN_PARENT_STAGE_CODE = 'feedback';
+const breakdownRows = ref<S0ContractReviewCycleBreakdownRow[]>([]);
+const breakdownLoading = ref(false);
+const breakdownDialogVisible = ref(false);
+const breakdownDialogTitle = ref('新增下钻');
+const breakdownEditingId = ref<number | null>(null);
+const breakdownSaving = ref(false);
+const breakdownFormRef = ref<FormInstance>();
+
 const filteredFactoryOptions = computed(() => {
 	if (!query.companyRefId) return factoryOptions.value;
 	return factoryOptions.value.filter((item) => item.pid === query.companyRefId);
@@ -199,6 +313,30 @@ const rules: FormRules = {
 	stageName: [{ required: true, message: '请填写阶段名称', trigger: 'blur' }],
 };
 
+function emptyBreakdownForm(): S0ContractReviewCycleBreakdownUpsert {
+	return {
+		companyRefId: undefined,
+		factoryRefId: undefined,
+		domainCode: '',
+		parentStageCode: BREAKDOWN_PARENT_STAGE_CODE,
+		groupCode: '',
+		groupName: '',
+		stdHours: 0,
+		orderNo: 1,
+		isActive: true,
+		createUser: '',
+		updateUser: '',
+	};
+}
+
+const breakdownForm = reactive<S0ContractReviewCycleBreakdownUpsert>(emptyBreakdownForm());
+
+const breakdownRules: FormRules = {
+	groupCode: [{ required: true, message: '请填写组编码', trigger: 'blur' }],
+	groupName: [{ required: true, message: '请填写组名称', trigger: 'blur' }],
+	stdHours: [{ required: true, message: '请填写标准时长', trigger: 'change' }],
+};
+
 function syncDomainCode() {
 	const f = factoryOptions.value.find((item) => item.id === form.factoryRefId);
 	form.domainCode = f?.code ?? '';
@@ -228,6 +366,15 @@ watch(
 	}
 );
 
+// 工厂切换时重新读取该工厂的 sync flag 和意见反馈下钻(未选工厂时清空)
+watch(
+	() => query.factoryRefId,
+	() => {
+		void loadSyncFlag();
+		void loadBreakdown();
+	}
+);
+
 async function loadOptions() {
 	const [companies, factories] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
 	companyOptions.value = companies;
@@ -338,9 +485,169 @@ function toggleActive(row: S0ContractReviewCycleRow) {
 		.catch(() => {});
 }
 
+// ── 同步开关加载与切换 ────────────────────────────────────────────────
+
+async function loadSyncFlag() {
+	if (!query.factoryRefId) {
+		// 未选工厂:reset 到关闭状态;不发请求
+		syncEnabled.value = false;
+		return;
+	}
+	try {
+		const data = await s0ContractReviewCycleSyncFlagApi.get({ factoryRefId: query.factoryRefId });
+		syncEnabled.value = data?.isSyncEnabled ?? false;
+	} catch {
+		syncEnabled.value = false;
+	}
+}
+
+async function onSyncChange(next: boolean | string | number) {
+	const nextBool = next === true;
+	if (!query.factoryRefId || !query.companyRefId) {
+		// 双保险:模板已 disabled,但工厂被外部清空时回滚
+		syncEnabled.value = !nextBool;
+		ElMessage.warning('请先选择公司与工厂');
+		return;
+	}
+	const factory = factoryOptions.value.find((item) => item.id === query.factoryRefId);
+	syncLoading.value = true;
+	try {
+		await s0ContractReviewCycleSyncFlagApi.update({
+			companyRefId: query.companyRefId,
+			factoryRefId: query.factoryRefId,
+			domainCode: factory?.code ?? undefined,
+			isSyncEnabled: nextBool,
+		});
+		syncEnabled.value = nextBool;
+		ElMessage.success(nextBool ? '已开启同步' : '已关闭同步');
+	} catch {
+		syncEnabled.value = !nextBool;
+		ElMessage.error('同步开关切换失败,请稍后重试');
+	} finally {
+		syncLoading.value = false;
+	}
+}
+
+// ── 意见反馈下钻加载与 CRUD ─────────────────────────────────────────
+
+async function loadBreakdown() {
+	if (!query.factoryRefId) {
+		breakdownRows.value = [];
+		return;
+	}
+	breakdownLoading.value = true;
+	try {
+		const data = await s0ContractReviewCycleBreakdownsApi.list({
+			factoryRefId: query.factoryRefId,
+			parentStageCode: BREAKDOWN_PARENT_STAGE_CODE,
+			page: 1,
+			pageSize: 100,
+		});
+		breakdownRows.value = data.list;
+	} catch {
+		breakdownRows.value = [];
+	} finally {
+		breakdownLoading.value = false;
+	}
+}
+
+function resetBreakdownForm() {
+	breakdownEditingId.value = null;
+	Object.assign(breakdownForm, emptyBreakdownForm());
+	breakdownFormRef.value?.clearValidate();
+}
+
+function openBreakdownCreate() {
+	if (!query.factoryRefId || !query.companyRefId) {
+		ElMessage.warning('请先选择公司与工厂');
+		return;
+	}
+	resetBreakdownForm();
+	const factory = factoryOptions.value.find((item) => item.id === query.factoryRefId);
+	breakdownForm.companyRefId = query.companyRefId;
+	breakdownForm.factoryRefId = query.factoryRefId;
+	breakdownForm.domainCode = factory?.code ?? '';
+	breakdownForm.orderNo = breakdownRows.value.length + 1;
+	breakdownDialogTitle.value = '新增下钻';
+	breakdownDialogVisible.value = true;
+}
+
+function openBreakdownEdit(row: S0ContractReviewCycleBreakdownRow) {
+	resetBreakdownForm();
+	breakdownEditingId.value = row.id;
+	breakdownDialogTitle.value = `编辑:${row.groupName}`;
+	Object.assign(breakdownForm, {
+		companyRefId: row.companyRefId,
+		factoryRefId: row.factoryRefId,
+		domainCode: row.domainCode ?? '',
+		parentStageCode: row.parentStageCode,
+		groupCode: row.groupCode,
+		groupName: row.groupName,
+		stdHours: row.stdHours,
+		orderNo: row.orderNo,
+		isActive: row.isActive,
+		createUser: row.createUser ?? '',
+		updateUser: row.updateUser ?? '',
+	});
+	breakdownDialogVisible.value = true;
+}
+
+async function submitBreakdownForm() {
+	await breakdownFormRef.value?.validate();
+	breakdownSaving.value = true;
+	try {
+		const payload: S0ContractReviewCycleBreakdownUpsert = { ...breakdownForm };
+		if (breakdownEditingId.value !== null) {
+			await s0ContractReviewCycleBreakdownsApi.update(breakdownEditingId.value, payload);
+			ElMessage.success('已保存');
+		} else {
+			await s0ContractReviewCycleBreakdownsApi.create(payload);
+			ElMessage.success('已创建');
+		}
+		breakdownDialogVisible.value = false;
+		await loadBreakdown();
+	} catch {
+		ElMessage.error('保存下钻配置失败,请检查填写内容后重试');
+	} finally {
+		breakdownSaving.value = false;
+	}
+}
+
+function onBreakdownDelete(row: S0ContractReviewCycleBreakdownRow) {
+	ElMessageBox.confirm(`确认删除该部门/组 PI 配置吗?「${row.groupName}」`, '确认', { type: 'warning' })
+		.then(async () => {
+			try {
+				await s0ContractReviewCycleBreakdownsApi.delete(row.id);
+				ElMessage.success('已删除');
+				await loadBreakdown();
+			} catch {
+				ElMessage.error('删除失败,请稍后重试');
+			}
+		})
+		.catch(() => {});
+}
+
+function breakdownToggleActive(row: S0ContractReviewCycleBreakdownRow) {
+	const next = !row.isActive;
+	const actionText = next ? '启用' : '禁用';
+	ElMessageBox.confirm(`确定${actionText}「${row.groupName}」?`, '确认', { type: 'warning' })
+		.then(async () => {
+			try {
+				await s0ContractReviewCycleBreakdownsApi.toggleEnabled(row.id, { isActive: next });
+				ElMessage.success(`${actionText}成功`);
+				await loadBreakdown();
+			} catch {
+				ElMessage.error('启停失败,请稍后重试');
+			}
+		})
+		.catch(() => {});
+}
+
 onMounted(async () => {
 	await loadOptions();
 	await loadList();
+	await loadSyncFlag();
+	await loadBreakdown();
 });
 </script>
 
@@ -356,4 +663,66 @@ onMounted(async () => {
 	display: flex;
 	justify-content: flex-end;
 }
+
+.sync-bar {
+	display: flex;
+	align-items: center;
+	gap: 12px;
+	margin-bottom: 12px;
+	padding: 10px 14px;
+	background: var(--el-color-info-light-9);
+	border-left: 4px solid var(--el-color-primary);
+	border-radius: 4px;
+
+	&__label {
+		font-weight: 600;
+		color: var(--el-text-color-primary);
+	}
+
+	&__text {
+		color: var(--el-text-color-secondary);
+		font-size: 13px;
+	}
+}
+
+.breakdown-section {
+	margin-top: 24px;
+	padding-top: 16px;
+	border-top: 1px dashed var(--el-border-color);
+
+	&__head {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		margin-bottom: 12px;
+		gap: 12px;
+	}
+
+	&__title {
+		margin: 0;
+		font-size: 16px;
+		font-weight: 600;
+		color: var(--el-text-color-primary);
+	}
+
+	&__actions {
+		display: flex;
+		gap: 8px;
+	}
+
+	&__hint {
+		padding: 24px 0;
+		text-align: center;
+		color: var(--el-text-color-secondary);
+		font-size: 13px;
+		background: var(--el-color-info-light-9);
+		border-radius: 4px;
+	}
+}
+
+:deep(.breakdown-section__wrap-cell .cell) {
+	white-space: normal;
+	word-break: break-word;
+	line-height: 1.4;
+}
 </style>

+ 6 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.154</AssemblyVersion>
-    <FileVersion>1.0.154</FileVersion>
-    <Version>1.0.154</Version>
+    <AssemblyVersion>1.0.155</AssemblyVersion>
+    <FileVersion>1.0.155</FileVersion>
+    <Version>1.0.155</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -109,6 +109,9 @@
     <None Update="UpdateScripts\1.0.154.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.155.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
 
   <ItemGroup>

+ 78 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.155.sql

@@ -0,0 +1,78 @@
+-- ──────────────────────────────────────────────────────────────────────
+-- S0-CONTRACT-REVIEW-CYCLE-PI-CONFIG-SYNC-1
+-- 1. 主表 S0ContractReviewCycle 现有 10 行 stage_code 从 CR1-CR5 → 语义编码
+--    (opinion_review/feedback/second_review/leader_opinion/sign)。
+--    并在 std_hours=0 时填初值(8/12/8/10/2),不覆盖用户已配置的非零值。
+-- 2. 新建 S0ContractReviewCycleBreakdown 下钻配置表。
+-- 3. 新建 S0ContractReviewCycleSyncFlag 工厂级同步开关表。
+-- 4. seed 意见反馈下钻 6 行(3 组 × 2 现有工厂)。
+--
+-- 幂等:
+--   - UPDATE WHERE 子句迁移后不匹配 → 第二次执行空操作;
+--   - std_hours 附加 AND std_hours=0 守卫,保留用户后续编辑;
+--   - CREATE TABLE IF NOT EXISTS;
+--   - INSERT IGNORE 跳过 (factory, parent_stage, group) 唯一冲突。
+-- ──────────────────────────────────────────────────────────────────────
+
+-- 1a. PI 初值填入(仅当 std_hours=0 时;保留用户已配置的非零值)
+UPDATE `S0ContractReviewCycle` SET `std_hours` = 8  WHERE `stage_code` = 'CR1' AND `std_hours` = 0;
+UPDATE `S0ContractReviewCycle` SET `std_hours` = 12 WHERE `stage_code` = 'CR2' AND `std_hours` = 0;
+UPDATE `S0ContractReviewCycle` SET `std_hours` = 8  WHERE `stage_code` = 'CR3' AND `std_hours` = 0;
+UPDATE `S0ContractReviewCycle` SET `std_hours` = 10 WHERE `stage_code` = 'CR4' AND `std_hours` = 0;
+UPDATE `S0ContractReviewCycle` SET `std_hours` = 2  WHERE `stage_code` = 'CR5' AND `std_hours` = 0;
+
+-- 1b. stage_code 语义化迁移(无条件,迁移后再跑空操作)
+UPDATE `S0ContractReviewCycle` SET `stage_code` = 'opinion_review' WHERE `stage_code` = 'CR1';
+UPDATE `S0ContractReviewCycle` SET `stage_code` = 'feedback'       WHERE `stage_code` = 'CR2';
+UPDATE `S0ContractReviewCycle` SET `stage_code` = 'second_review'  WHERE `stage_code` = 'CR3';
+UPDATE `S0ContractReviewCycle` SET `stage_code` = 'leader_opinion' WHERE `stage_code` = 'CR4';
+UPDATE `S0ContractReviewCycle` SET `stage_code` = 'sign'           WHERE `stage_code` = 'CR5';
+
+-- 2a. 下钻配置表
+CREATE TABLE IF NOT EXISTS `S0ContractReviewCycleBreakdown` (
+  `rec_id`            BIGINT       NOT NULL AUTO_INCREMENT,
+  `company_ref_id`    BIGINT       NOT NULL,
+  `factory_ref_id`    BIGINT       NOT NULL,
+  `domain_code`       VARCHAR(50)  NULL,
+  `parent_stage_code` VARCHAR(50)  NOT NULL,
+  `group_code`        VARCHAR(50)  NOT NULL,
+  `group_name`        VARCHAR(200) NOT NULL,
+  `std_hours`         DECIMAL(6,2) NOT NULL DEFAULT 0,
+  `order_no`          INT          NOT NULL DEFAULT 1,
+  `is_active`         TINYINT(1)   NOT NULL DEFAULT 1,
+  `create_user`       VARCHAR(100) NULL,
+  `create_time`       DATETIME     NOT NULL,
+  `update_user`       VARCHAR(100) NULL,
+  `update_time`       DATETIME     NULL,
+  PRIMARY KEY (`rec_id`),
+  UNIQUE KEY `uk_S0CRCBreakdown_factory_parent_group` (`factory_ref_id`, `parent_stage_code`, `group_code`),
+  KEY `idx_S0CRCBreakdown_factory_parent` (`factory_ref_id`, `parent_stage_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合同评审周期下钻配置(按 parent_stage_code 分组)';
+
+-- 2b. 工厂级同步开关表
+CREATE TABLE IF NOT EXISTS `S0ContractReviewCycleSyncFlag` (
+  `rec_id`          BIGINT       NOT NULL AUTO_INCREMENT,
+  `company_ref_id`  BIGINT       NOT NULL,
+  `factory_ref_id`  BIGINT       NOT NULL,
+  `domain_code`     VARCHAR(50)  NULL,
+  `is_sync_enabled` TINYINT(1)   NOT NULL DEFAULT 0,
+  `create_user`     VARCHAR(100) NULL,
+  `create_time`     DATETIME     NOT NULL,
+  `update_user`     VARCHAR(100) NULL,
+  `update_time`     DATETIME     NULL,
+  PRIMARY KEY (`rec_id`),
+  UNIQUE KEY `uk_S0CRCSyncFlag_factory` (`factory_ref_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合同评审周期同步开关(工厂级一行)';
+
+-- 3. 意见反馈下钻 seed(3 组 × 2 现有工厂 = 6 行),INSERT IGNORE 跳过唯一冲突
+INSERT IGNORE INTO `S0ContractReviewCycleBreakdown` (
+  `company_ref_id`, `factory_ref_id`, `domain_code`, `parent_stage_code`,
+  `group_code`, `group_name`, `std_hours`, `order_no`, `is_active`,
+  `create_user`, `create_time`
+) VALUES
+  (1329900200001, 1329900200002, NULL, 'feedback', 'market',        '市场部',     4.80, 1, 1, 'system', NOW()),
+  (1329900200001, 1329900200002, NULL, 'feedback', 'tech_presales', '技术售前组', 4.20, 2, 1, 'system', NOW()),
+  (1329900200001, 1329900200002, NULL, 'feedback', 'planning',      '综合计划',   3.00, 3, 1, 'system', NOW()),
+  (1329900200001, 1329900200003, NULL, 'feedback', 'market',        '市场部',     4.80, 1, 1, 'system', NOW()),
+  (1329900200001, 1329900200003, NULL, 'feedback', 'tech_presales', '技术售前组', 4.20, 2, 1, 'system', NOW()),
+  (1329900200001, 1329900200003, NULL, 'feedback', 'planning',      '综合计划',   3.00, 3, 1, 'system', NOW());

+ 121 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ContractReviewCycleBreakdownsController.cs

@@ -0,0 +1,121 @@
+using Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
+
+/// <summary>
+/// 合同评审周期下钻配置 CRUD(按 parent_stage_code + group_code 分组)。
+/// 本批仅维护 parent_stage_code='feedback'(意见反馈)下的部门/组 PI 标准。
+/// </summary>
+[ApiController]
+[Route("api/s0/sales/contract-review-cycle-breakdowns")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0ContractReviewCycleBreakdownsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0ContractReviewCycleBreakdown> _rep;
+
+    public AdoS0ContractReviewCycleBreakdownsController(SqlSugarRepository<AdoS0ContractReviewCycleBreakdown> rep)
+        => _rep = rep;
+
+    [HttpGet]
+    public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0ContractReviewCycleBreakdownQueryDto q)
+    {
+        (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
+
+        var query = _rep.AsQueryable()
+            .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
+            .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
+            .WhereIF(!string.IsNullOrWhiteSpace(q.ParentStageCode), x => x.ParentStageCode == q.ParentStageCode)
+            .WhereIF(q.IsActive.HasValue, x => x.IsActive == q.IsActive!.Value);
+
+        var total = await query.CountAsync();
+        var list = await query.OrderBy(x => x.OrderNo).Skip((q.Page - 1) * q.PageSize).Take(q.PageSize).ToListAsync();
+        return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
+    }
+
+    [HttpGet("{id:long}")]
+    public async Task<IActionResult> GetAsync(long id)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        return item == null ? NotFound() : Ok(item);
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> CreateAsync([FromBody] AdoS0ContractReviewCycleBreakdownUpsertDto dto)
+    {
+        if (await _rep.IsAnyAsync(x =>
+                x.FactoryRefId == dto.FactoryRefId &&
+                x.ParentStageCode == dto.ParentStageCode &&
+                x.GroupCode == dto.GroupCode))
+            return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "该工厂此父阶段下的组编码已存在");
+
+        var entity = new AdoS0ContractReviewCycleBreakdown
+        {
+            CompanyRefId = dto.CompanyRefId,
+            FactoryRefId = dto.FactoryRefId,
+            DomainCode = dto.DomainCode,
+            ParentStageCode = dto.ParentStageCode,
+            GroupCode = dto.GroupCode,
+            GroupName = dto.GroupName,
+            StdHours = dto.StdHours,
+            OrderNo = dto.OrderNo,
+            IsActive = dto.IsActive,
+            CreateUser = dto.CreateUser,
+            CreateTime = DateTime.Now,
+        };
+        await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        return Ok(entity);
+    }
+
+    [HttpPut("{id:long}")]
+    public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0ContractReviewCycleBreakdownUpsertDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+
+        if (await _rep.IsAnyAsync(x =>
+                x.Id != id &&
+                x.FactoryRefId == dto.FactoryRefId &&
+                x.ParentStageCode == dto.ParentStageCode &&
+                x.GroupCode == dto.GroupCode))
+            return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "该工厂此父阶段下的组编码已存在");
+
+        entity.CompanyRefId = dto.CompanyRefId;
+        entity.FactoryRefId = dto.FactoryRefId;
+        entity.DomainCode = dto.DomainCode;
+        entity.ParentStageCode = dto.ParentStageCode;
+        entity.GroupCode = dto.GroupCode;
+        entity.GroupName = dto.GroupName;
+        entity.StdHours = dto.StdHours;
+        entity.OrderNo = dto.OrderNo;
+        entity.IsActive = dto.IsActive;
+        entity.UpdateUser = dto.UpdateUser;
+        entity.UpdateTime = DateTime.Now;
+
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpPatch("{id:long}/toggle-enabled")]
+    public async Task<IActionResult> ToggleActiveAsync(long id, [FromBody] AdoS0ContractReviewCycleBreakdownToggleActiveDto dto)
+    {
+        var entity = await _rep.GetByIdAsync(id);
+        if (entity == null) return NotFound();
+        entity.IsActive = dto.IsActive;
+        entity.UpdateTime = DateTime.Now;
+        await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        return Ok(entity);
+    }
+
+    [HttpDelete("{id:long}")]
+    public async Task<IActionResult> DeleteAsync(long id)
+    {
+        var item = await _rep.GetByIdAsync(id);
+        if (item == null) return NotFound();
+        await _rep.DeleteAsync(item);
+        return Ok(new { message = "删除成功" });
+    }
+}

+ 89 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ContractReviewCycleSyncFlagsController.cs

@@ -0,0 +1,89 @@
+using Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
+
+/// <summary>
+/// 合同评审周期同步开关:工厂级一行。
+/// GET 行不存在时返回 IsSyncEnabled=false 兜底;PUT 走 upsert。
+/// </summary>
+[ApiController]
+[Route("api/s0/sales/contract-review-cycle-sync-flag")]
+[AllowAnonymous]
+[NonUnify]
+public class AdoS0ContractReviewCycleSyncFlagsController : ControllerBase
+{
+    private readonly SqlSugarRepository<AdoS0ContractReviewCycleSyncFlag> _rep;
+
+    public AdoS0ContractReviewCycleSyncFlagsController(SqlSugarRepository<AdoS0ContractReviewCycleSyncFlag> rep)
+        => _rep = rep;
+
+    [HttpGet]
+    public async Task<IActionResult> GetByFactoryAsync([FromQuery] long factoryRefId)
+    {
+        if (factoryRefId <= 0) return BadRequest(new { message = "factoryRefId 不能为空" });
+
+        var entity = await _rep.AsQueryable()
+            .Where(x => x.FactoryRefId == factoryRefId)
+            .FirstAsync();
+
+        if (entity == null)
+        {
+            return Ok(new AdoS0ContractReviewCycleSyncFlagDto
+            {
+                FactoryRefId = factoryRefId,
+                IsSyncEnabled = false,
+            });
+        }
+
+        return Ok(new AdoS0ContractReviewCycleSyncFlagDto
+        {
+            Id = entity.Id,
+            CompanyRefId = entity.CompanyRefId,
+            FactoryRefId = entity.FactoryRefId,
+            DomainCode = entity.DomainCode,
+            IsSyncEnabled = entity.IsSyncEnabled,
+        });
+    }
+
+    [HttpPut]
+    public async Task<IActionResult> UpsertAsync([FromBody] AdoS0ContractReviewCycleSyncFlagUpsertDto dto)
+    {
+        var now = DateTime.Now;
+        var entity = await _rep.AsQueryable()
+            .Where(x => x.FactoryRefId == dto.FactoryRefId)
+            .FirstAsync();
+
+        if (entity == null)
+        {
+            entity = new AdoS0ContractReviewCycleSyncFlag
+            {
+                CompanyRefId = dto.CompanyRefId,
+                FactoryRefId = dto.FactoryRefId,
+                DomainCode = dto.DomainCode,
+                IsSyncEnabled = dto.IsSyncEnabled,
+                CreateUser = dto.CreateUser,
+                CreateTime = now,
+            };
+            await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
+        }
+        else
+        {
+            entity.CompanyRefId = dto.CompanyRefId;
+            entity.DomainCode = dto.DomainCode;
+            entity.IsSyncEnabled = dto.IsSyncEnabled;
+            entity.UpdateUser = dto.UpdateUser;
+            entity.UpdateTime = now;
+            await _rep.AsUpdateable(entity).ExecuteCommandAsync();
+        }
+
+        return Ok(new AdoS0ContractReviewCycleSyncFlagDto
+        {
+            Id = entity.Id,
+            CompanyRefId = entity.CompanyRefId,
+            FactoryRefId = entity.FactoryRefId,
+            DomainCode = entity.DomainCode,
+            IsSyncEnabled = entity.IsSyncEnabled,
+        });
+    }
+}

+ 60 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0ContractReviewCyclesController.cs

@@ -11,9 +11,18 @@ namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
 public class AdoS0ContractReviewCyclesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0ContractReviewCycle> _rep;
+    private readonly SqlSugarRepository<AdoS0ContractReviewCycleSyncFlag> _syncRep;
+    private readonly SqlSugarRepository<AdoS0ContractReviewCycleBreakdown> _breakdownRep;
 
-    public AdoS0ContractReviewCyclesController(SqlSugarRepository<AdoS0ContractReviewCycle> rep)
-        => _rep = rep;
+    public AdoS0ContractReviewCyclesController(
+        SqlSugarRepository<AdoS0ContractReviewCycle> rep,
+        SqlSugarRepository<AdoS0ContractReviewCycleSyncFlag> syncRep,
+        SqlSugarRepository<AdoS0ContractReviewCycleBreakdown> breakdownRep)
+    {
+        _rep = rep;
+        _syncRep = syncRep;
+        _breakdownRep = breakdownRep;
+    }
 
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0ContractReviewCycleQueryDto q)
@@ -104,4 +113,53 @@ public class AdoS0ContractReviewCyclesController : ControllerBase
         await _rep.DeleteAsync(item);
         return Ok(new { message = "删除成功" });
     }
+
+    /// <summary>
+    /// PI 只读配置:合并主阶段 + 意见反馈下钻,仅当工厂同步开关开启时返回数据。
+    /// 为 S8 后续接入预留契约(S8-ORDER-REVIEW-PI-FROM-S0-1),本批不消费。
+    /// </summary>
+    [HttpGet("pi-config")]
+    public async Task<IActionResult> GetPiConfigAsync([FromQuery] long factoryRefId)
+    {
+        if (factoryRefId <= 0) return BadRequest(new { message = "factoryRefId 不能为空" });
+
+        var syncFlag = await _syncRep.AsQueryable()
+            .Where(x => x.FactoryRefId == factoryRefId)
+            .FirstAsync();
+        var syncEnabled = syncFlag?.IsSyncEnabled ?? false;
+
+        var result = new AdoS0ContractReviewCyclePiConfigDto
+        {
+            FactoryRefId = factoryRefId,
+            SyncEnabled = syncEnabled,
+        };
+
+        if (!syncEnabled) return Ok(result);
+
+        var stages = await _rep.AsQueryable()
+            .Where(x => x.FactoryRefId == factoryRefId && x.IsActive)
+            .OrderBy(x => x.OrderNo)
+            .ToListAsync();
+        result.MainStages = stages.Select(x => new AdoS0ContractReviewCyclePiConfigStageDto
+        {
+            StageCode = x.StageCode,
+            StageName = x.StageName,
+            StdHours = x.StdHours,
+            OrderNo = x.OrderNo,
+        }).ToList();
+
+        var breakdowns = await _breakdownRep.AsQueryable()
+            .Where(x => x.FactoryRefId == factoryRefId && x.ParentStageCode == "feedback" && x.IsActive)
+            .OrderBy(x => x.OrderNo)
+            .ToListAsync();
+        result.OpinionFeedbackBreakdown = breakdowns.Select(x => new AdoS0ContractReviewCyclePiConfigBreakdownDto
+        {
+            GroupCode = x.GroupCode,
+            GroupName = x.GroupName,
+            StdHours = x.StdHours,
+            OrderNo = x.OrderNo,
+        }).ToList();
+
+        return Ok(result);
+    }
 }

+ 99 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Sales/AdoS0SalesDtos.cs

@@ -393,3 +393,102 @@ public class AdoS0OrderReviewCycleToggleActiveDto
 {
     public bool IsActive { get; set; }
 }
+
+// ── B1.1: 合同评审周期下钻配置 ────────────────────────────────────────
+
+public class AdoS0ContractReviewCycleBreakdownQueryDto
+{
+    public long? CompanyRefId { get; set; }
+    public long? FactoryRefId { get; set; }
+    public string? DomainCode { get; set; }
+    /// <summary>父阶段编码(如 'feedback' 意见反馈)</summary>
+    public string? ParentStageCode { get; set; }
+    public bool? IsActive { get; set; }
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 20;
+}
+
+public class AdoS0ContractReviewCycleBreakdownUpsertDto
+{
+    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    public long CompanyRefId { get; set; }
+
+    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    public long FactoryRefId { get; set; }
+
+    public string? DomainCode { get; set; }
+
+    [Required(ErrorMessage = "父阶段编码不能为空")]
+    public string ParentStageCode { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "组编码不能为空")]
+    public string GroupCode { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "组名称不能为空")]
+    public string GroupName { get; set; } = string.Empty;
+
+    [Range(typeof(decimal), "0", "9999.99", ErrorMessage = "标准时长必须在 0 ~ 9999.99 之间")]
+    public decimal StdHours { get; set; }
+
+    public int OrderNo { get; set; } = 1;
+    public bool IsActive { get; set; } = true;
+    public string? CreateUser { get; set; }
+    public string? UpdateUser { get; set; }
+}
+
+public class AdoS0ContractReviewCycleBreakdownToggleActiveDto
+{
+    public bool IsActive { get; set; }
+}
+
+// ── B1.2: 合同评审周期同步开关 ────────────────────────────────────────
+
+/// <summary>同步开关读取响应;行不存在时 IsSyncEnabled 默认 false。</summary>
+public class AdoS0ContractReviewCycleSyncFlagDto
+{
+    public long? Id { get; set; }
+    public long? CompanyRefId { get; set; }
+    public long FactoryRefId { get; set; }
+    public string? DomainCode { get; set; }
+    public bool IsSyncEnabled { get; set; }
+}
+
+public class AdoS0ContractReviewCycleSyncFlagUpsertDto
+{
+    [Range(1, long.MaxValue, ErrorMessage = "公司不能为空")]
+    public long CompanyRefId { get; set; }
+
+    [Range(1, long.MaxValue, ErrorMessage = "工厂不能为空")]
+    public long FactoryRefId { get; set; }
+
+    public string? DomainCode { get; set; }
+    public bool IsSyncEnabled { get; set; }
+    public string? CreateUser { get; set; }
+    public string? UpdateUser { get; set; }
+}
+
+// ── B1.3: PI 只读配置(S8 后续可消费) ─────────────────────────────────
+
+public class AdoS0ContractReviewCyclePiConfigStageDto
+{
+    public string StageCode { get; set; } = string.Empty;
+    public string StageName { get; set; } = string.Empty;
+    public int StdHours { get; set; }
+    public int OrderNo { get; set; }
+}
+
+public class AdoS0ContractReviewCyclePiConfigBreakdownDto
+{
+    public string GroupCode { get; set; } = string.Empty;
+    public string GroupName { get; set; } = string.Empty;
+    public decimal StdHours { get; set; }
+    public int OrderNo { get; set; }
+}
+
+public class AdoS0ContractReviewCyclePiConfigDto
+{
+    public long FactoryRefId { get; set; }
+    public bool SyncEnabled { get; set; }
+    public List<AdoS0ContractReviewCyclePiConfigStageDto> MainStages { get; set; } = new();
+    public List<AdoS0ContractReviewCyclePiConfigBreakdownDto> OpinionFeedbackBreakdown { get; set; } = new();
+}

+ 60 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ContractReviewCycleBreakdown.cs

@@ -0,0 +1,60 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+
+/// <summary>
+/// 合同评审周期下钻配置(按 parent_stage_code 分组)。
+/// 本批仅初始化 parent_stage_code = 'feedback'(意见反馈)下的部门/组 PI 标准。
+/// </summary>
+[SugarTable("S0ContractReviewCycleBreakdown", "合同评审周期下钻配置")]
+[SugarIndex("uk_S0CRCBreakdown_factory_parent_group",
+    nameof(FactoryRefId), OrderByType.Asc,
+    nameof(ParentStageCode), OrderByType.Asc,
+    nameof(GroupCode), OrderByType.Asc,
+    IsUnique = true)]
+[SugarIndex("idx_S0CRCBreakdown_factory_parent",
+    nameof(FactoryRefId), OrderByType.Asc,
+    nameof(ParentStageCode), OrderByType.Asc,
+    IsUnique = false)]
+public class AdoS0ContractReviewCycleBreakdown
+{
+    [SugarColumn(ColumnName = "rec_id", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "company_ref_id", ColumnDataType = "bigint")]
+    public long CompanyRefId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_ref_id", ColumnDataType = "bigint")]
+    public long FactoryRefId { get; set; }
+
+    [SugarColumn(ColumnName = "domain_code", Length = 50, IsNullable = true)]
+    public string? DomainCode { get; set; }
+
+    [SugarColumn(ColumnName = "parent_stage_code", Length = 50)]
+    public string ParentStageCode { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "group_code", Length = 50)]
+    public string GroupCode { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "group_name", Length = 200)]
+    public string GroupName { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "std_hours", ColumnDataType = "decimal(6,2)")]
+    public decimal StdHours { get; set; }
+
+    [SugarColumn(ColumnName = "order_no")]
+    public int OrderNo { get; set; }
+
+    [SugarColumn(ColumnName = "is_active", ColumnDataType = "boolean")]
+    public bool IsActive { get; set; } = true;
+
+    [SugarColumn(ColumnName = "create_user", Length = 100, IsNullable = true)]
+    public string? CreateUser { get; set; }
+
+    [SugarColumn(ColumnName = "create_time")]
+    public DateTime CreateTime { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "update_user", Length = 100, IsNullable = true)]
+    public string? UpdateUser { get; set; }
+
+    [SugarColumn(ColumnName = "update_time", IsNullable = true)]
+    public DateTime? UpdateTime { get; set; }
+}

+ 40 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ContractReviewCycleSyncFlag.cs

@@ -0,0 +1,40 @@
+namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
+
+/// <summary>
+/// 合同评审周期同步开关(工厂级一行)。
+/// 行不存在 = 未开启(默认 false);行存在则以 IsSyncEnabled 字段为准。
+/// 开启后该工厂的 S0 合同评审周期配置(主阶段 + 下钻)将被标记为 S8 可消费的 PI 标准。
+/// </summary>
+[SugarTable("S0ContractReviewCycleSyncFlag", "合同评审周期同步开关")]
+[SugarIndex("uk_S0CRCSyncFlag_factory",
+    nameof(FactoryRefId), OrderByType.Asc,
+    IsUnique = true)]
+public class AdoS0ContractReviewCycleSyncFlag
+{
+    [SugarColumn(ColumnName = "rec_id", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "company_ref_id", ColumnDataType = "bigint")]
+    public long CompanyRefId { get; set; }
+
+    [SugarColumn(ColumnName = "factory_ref_id", ColumnDataType = "bigint")]
+    public long FactoryRefId { get; set; }
+
+    [SugarColumn(ColumnName = "domain_code", Length = 50, IsNullable = true)]
+    public string? DomainCode { get; set; }
+
+    [SugarColumn(ColumnName = "is_sync_enabled", ColumnDataType = "boolean")]
+    public bool IsSyncEnabled { get; set; } = false;
+
+    [SugarColumn(ColumnName = "create_user", Length = 100, IsNullable = true)]
+    public string? CreateUser { get; set; }
+
+    [SugarColumn(ColumnName = "create_time")]
+    public DateTime CreateTime { get; set; } = DateTime.Now;
+
+    [SugarColumn(ColumnName = "update_user", Length = 100, IsNullable = true)]
+    public string? UpdateUser { get; set; }
+
+    [SugarColumn(ColumnName = "update_time", IsNullable = true)]
+    public DateTime? UpdateTime { get; set; }
+}