feat/m3-increments #1
@ -1,12 +1,13 @@
|
||||
# 后端
|
||||
|
||||
> AI摘要:衍水集团营销人员工资管理系统的后端服务。FastAPI + SQLAlchemy(同步)+ MySQL + Redis,Python 3.12 由 uv 锁定。六层结构 `api / services / repositories / models / schemas / core`。当前实现登录鉴权与系统管理(用户/角色/操作日志/日志查询),业务模块待需求澄清后开发。
|
||||
> AI摘要:衍水集团营销人员工资管理系统的后端服务。FastAPI + SQLAlchemy(同步)+ MySQL + Redis,Python 3.12 由 uv 锁定。六层结构 `api / services / repositories / models / schemas / core`。已实现登录鉴权、系统管理(用户/角色/操作日志/日志查询)与 M1 参数体系(岗位角色管理、计提参数版本化/留痕/对比/预览/导出,2026-10-13);业务模块(基础/业务数据、核算、报表)按路线图推进。
|
||||
|
||||
## 职责
|
||||
|
||||
- 提供 REST 接口(统一前缀 `/api`)
|
||||
- 登录鉴权:图形验证码、JWT、登录限流、登出黑名单、登录日志
|
||||
- 系统管理(需求 2.5.1):用户管理、角色管理、操作日志(自动审计)、操作与登录日志查询
|
||||
- 系统管理(需求 2.5.1):用户管理、角色管理、岗位角色(#28 可扩展+公式模板绑定)、操作日志(自动审计)、操作与登录日志查询
|
||||
- 计提参数体系(需求 #28):16 项参数目录、按生效日期版本化(回款到账日取值)、变更批次留痕/对比/导出、计算预览
|
||||
- 后续承担:基础数据、业务数据、绩效核算、报表统计、T+ 数据同步
|
||||
|
||||
## 技术栈与版本
|
||||
|
||||
@ -81,6 +81,17 @@ uv run pytest tests/integration -q # 仅集成
|
||||
| `integration/test_health.py` | 统一信封与全局异常 | 7 | `/api/health` 始终 200 且依赖状态如实上报;404/405 返回统一信封而非框架默认 `detail` 数组;`/api/docs` 可访问 |
|
||||
| `integration/test_schema_sync.py` | DDL 不漂移 | 6 | `db/migration/V1__*.sql` 与 `db/init/init-dev.sql` 的**表集合与列集合逐表比对**——两份 DDL 手工同步迟早漂移,且漂移后果隐蔽(开发库结构与生产不一致,上线才发现) |
|
||||
|
||||
### M1 计提参数与岗位角色(2026-10-13 完成)
|
||||
|
||||
| 文件 | 守什么 | 用例数 |
|
||||
| --- | --- | --- |
|
||||
| `tests/integration/test_business_roles.py` | 岗位 CRUD 链路、内置岗位禁删禁停(种子 3 个)、模板编码校验、编辑换模板、权限 403、模板下拉 5 项 | 8 |
|
||||
| `tests/integration/test_param_management.py` | 目录 16 项与七分组、企业户 2‰→3‰ 按到账日回溯、斜率 4/360 分数原样存储、变更生命周期(立即/未来生效不泄漏/同日取最新/撤回回退/已生效禁撤)、值校验(上限/枚举/除零分数/过去日期)、预览金标准(27 天=97/10000、折算 15000、绩效 145.5、可付 52.38;361 天=4‰)、经理聚合口径拒绝、权限 403 | 18 |
|
||||
| `tests/unit/test_formula.py`、`tests/unit/test_param_registry.py` | 八个公式纯函数精确分数断言(第 1 天=899/90000、360=6‰、361=4‰、27 天、防除零、负绩效不归零、R×R)、注册表 16 项与迁移种子同源、模板声明参数闭环 | 43 |
|
||||
| `tests/unit/test_param_snapshot.py` | 快照覆盖链(角色>产品类别>默认)、注册表默认值兜底、同日取最新、冻结不可变 | 12 |
|
||||
|
||||
运行:`uv run pytest -q` → **406 passed**(2026-10-13 全量含回归;T5 冒烟另验证提交/生效/撤回/对比/导出/预览全链路)。
|
||||
|
||||
## 三、运行记录
|
||||
|
||||
| 日期 | 范围 | 用例数 | 结果 | 备注 |
|
||||
@ -114,9 +125,9 @@ uv run pytest tests/integration -q # 仅集成
|
||||
|
||||
| 模块 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 基础数据管理 | 未开始 | 依赖 T+ 对接方式确认 |
|
||||
| 业务数据管理 | 未开始 | 依赖需求澄清(回款天数口径等 10 项) |
|
||||
| 绩效工资核算 | 未开始 | 依赖需求澄清;金额精度需专项用例(`Fraction` 与 1/90 日斜率) |
|
||||
| 基础数据管理 | 未开始 | 需求已澄清(2026-10-12);依赖 T+ 对接方式确认 |
|
||||
| 业务数据管理 | 未开始 | 需求已澄清(28 项裁决见 `docs/问题与解决方案对照表_v2.xlsx`) |
|
||||
| 绩效工资核算 | 未开始 | 需求已澄清;金额精度需专项用例(`Fraction` 与 1/90 日斜率已在 M1 公式层覆盖) |
|
||||
| 报表统计 | 未开始 | 依赖报表口径确认 |
|
||||
|
||||
> 系统管理模块(用户/角色/日志查询)已于 2026-10-10 完成并补齐用例,见上表。
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
# 前端
|
||||
|
||||
> AI摘要:衍水集团营销人员工资管理系统的前端。Vue 3 + TypeScript + Vite + Element Plus + Pinia。关键文件 `src/api/request.ts`(统一请求封装)、`src/components/layout/BasicLayout.vue`(应用布局与导航)、`src/router/index.ts`(路由守卫)、`src/store/user.ts`(会话状态)。当前完成登录链路与系统管理页面(用户/角色/日志查询),业务模块页面待接入。
|
||||
> AI摘要:衍水集团营销人员工资管理系统的前端。Vue 3 + TypeScript + Vite + Element Plus + Pinia。关键文件 `src/api/request.ts`(统一请求封装)、`src/components/layout/BasicLayout.vue`(应用布局与导航)、`src/router/index.ts`(路由守卫)、`src/store/user.ts`(会话状态)。已实现登录链路(2026-10 视觉方向重做:点阵底/涟漪/天光/居中卡片)、系统管理页面、岗位角色与计提参数配置页、Bento 仪表盘首页;视觉体系与设计预览见 `docs/design/视觉方向预览.html`。
|
||||
|
||||
## 职责
|
||||
|
||||
- 登录页与图形验证码交互
|
||||
- 会话状态管理与路由守卫
|
||||
- 岗位角色管理页(模板绑定)、计提参数配置页(分组目录/批次提交/版本对比/导出/计算预览)
|
||||
- 首页 Bento 仪表盘(M1 空态设计;业务上线后接入真实统计,图表用 ECharts)
|
||||
- 后续:基础数据、业务数据、绩效核算、报表等业务页面
|
||||
|
||||
## 技术栈与版本约束
|
||||
@ -136,3 +138,13 @@ E2E_ADMIN_PASSWORD=<开发库admin密码> python e2e/system_management.py
|
||||
**验证码图片不显示** —— 多为代理未生效。访问 `http://localhost:5173/api/health` 应返回统一信封格式 JSON。
|
||||
|
||||
**`pnpm install` 报 peer 依赖错误** —— 确认 `@vue/devtools-api` 在 `dependencies` 中(pinia 4 的必需 peer)。
|
||||
|
||||
## 端到端测试(2026-10-13 追加 · M1 参数体系)
|
||||
|
||||
| 脚本 | 覆盖 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| `e2e/login_flow.py` | 登录链路 8 项 | ✓ 全通过(登录页视觉重做后回归) |
|
||||
| `e2e/system_management.py` | 系统管理 13 项 | ✓ 全通过(布局白化后回归) |
|
||||
| `e2e/param_management.py` | 计提参数 7 步:目录分组 → 修改入待提交 → 批次提交(立即生效)→ 目录反映新值 → 版本记录 → 恢复种子值 → 计算预览金标准(27 天 / 9.7‰ / 145.5 元) | ✓ 全通过(首跑) |
|
||||
|
||||
运行前置:容器 + 后端(8000)+ 前端 dev(5173);`E2E_ADMIN_PASSWORD` 传管理员密码。
|
||||
|
||||
141
frontend/e2e/param_management.py
Normal file
141
frontend/e2e/param_management.py
Normal file
@ -0,0 +1,141 @@
|
||||
"""计提参数管理端到端测试(需要容器、后端、前端都在运行)。
|
||||
|
||||
覆盖链路(M1 验收):目录分组展示 → 修改参数入「待提交」→ 批次提交(立即
|
||||
生效)→ 目录反映新值 → 版本记录可见 → 计算预览金标准复现(27 天 / 9.7‰ /
|
||||
145.5 元)→ 值改回种子值恢复现场。
|
||||
|
||||
运行:``E2E_ADMIN_PASSWORD=... python e2e/param_management.py``
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, __import__("os").path.dirname(__file__))
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
from common import (
|
||||
BASE_URL,
|
||||
E2EFailure,
|
||||
collect_console_errors,
|
||||
expect_toast,
|
||||
fail,
|
||||
login,
|
||||
wait_toast,
|
||||
)
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
PARAMS_URL = f"{BASE_URL}/system/params"
|
||||
console_errors: list[str] = []
|
||||
|
||||
|
||||
def step(title: str) -> None:
|
||||
print(f"—— {title}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failures: list[str] = []
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 860})
|
||||
errors = collect_console_errors(page, console_errors)
|
||||
|
||||
try:
|
||||
login(page, "admin", __import__("os").environ.get("E2E_ADMIN_PASSWORD", "E2ePwd@2026"))
|
||||
page.goto(PARAMS_URL, wait_until="networkidle")
|
||||
|
||||
step("1. 目录分组展示")
|
||||
if page.locator(".group-title", has_text="支付环节").count() == 0:
|
||||
fail("目录分组", "未找到「支付环节」分组")
|
||||
# 展开「支付环节」分组
|
||||
page.locator(".el-collapse-item__header", has_text="支付环节").click()
|
||||
page.wait_for_timeout(300)
|
||||
|
||||
step("2. 修改预付款比例 10 → 12(立即生效)")
|
||||
row = page.locator(".param-table tbody tr", has_text="预付款计提比例").first
|
||||
row.get_by_role("button", name="修改").click()
|
||||
dialog = page.locator(".el-dialog")
|
||||
dialog.locator("input").fill("12")
|
||||
dialog.get_by_role("button", name="加入待提交").click()
|
||||
if "待改为 12" not in page.content():
|
||||
fail("待提交", "参数行未出现「待改为 12」标记")
|
||||
|
||||
step("3. 提交批次")
|
||||
page.get_by_role("button", name="提交变更").click()
|
||||
page.locator(".el-message-box").get_by_role("button", name="提交").click()
|
||||
expect_toast(page, "已提交", "提交批次")
|
||||
|
||||
step("4. 目录反映新值")
|
||||
page.wait_for_timeout(500)
|
||||
row = page.locator(".param-table tbody tr", has_text="预付款计提比例").first
|
||||
if "12" not in row.text_content():
|
||||
fail("目录生效", "提交后目录未显示新值 12")
|
||||
|
||||
step("5. 版本记录可见")
|
||||
page.get_by_role("button", name="版本管理").click()
|
||||
page.wait_for_timeout(500)
|
||||
first_version = page.locator(".el-drawer .el-table tbody tr").first.text_content()
|
||||
if "P2026" not in first_version:
|
||||
fail("版本记录", f"最新记录异常:{first_version}")
|
||||
page.keyboard.press("Escape")
|
||||
page.wait_for_timeout(300)
|
||||
|
||||
step("6. 恢复种子值 10")
|
||||
row = page.locator(".param-table tbody tr", has_text="预付款计提比例").first
|
||||
row.get_by_role("button", name="修改").click()
|
||||
dialog = page.locator(".el-dialog")
|
||||
dialog.locator("input").fill("10")
|
||||
dialog.get_by_role("button", name="加入待提交").click()
|
||||
page.get_by_role("button", name="提交变更").click()
|
||||
page.locator(".el-message-box").get_by_role("button", name="提交").click()
|
||||
wait_toast(page, "已提交")
|
||||
page.wait_for_timeout(400)
|
||||
|
||||
step("7. 计算预览金标准(27 天 / 9.7‰ / 145.5)")
|
||||
page.get_by_role("button", name="计算预览").click()
|
||||
page.wait_for_timeout(600)
|
||||
# 多个抽屉组件常驻 DOM(隐藏未销毁),按标题过滤出预览抽屉
|
||||
drawer = page.locator(".el-drawer").filter(has_text="计算预览").last
|
||||
# 岗位下拉默认 salesperson(v-model 初始值),无需操作
|
||||
drawer.locator(".el-form-item", has_text="发货日").locator("input").click()
|
||||
page.keyboard.type("2025-12-15")
|
||||
page.keyboard.press("Enter")
|
||||
drawer.locator(".el-form-item", has_text="回款到账日").locator("input").click()
|
||||
page.keyboard.type("2026-01-10")
|
||||
page.keyboard.press("Enter")
|
||||
drawer.locator(".el-form-item", has_text="销售额").locator("input").fill("100000")
|
||||
drawer.locator(".el-form-item", has_text="毛利润额").locator("input").fill("45000")
|
||||
drawer.locator(".el-form-item", has_text="本笔回款").locator("input").fill("60000")
|
||||
drawer.get_by_role("button", name="试 算").click()
|
||||
page.wait_for_timeout(900)
|
||||
result_text = drawer.locator(".preview-result").text_content()
|
||||
for expected in ["27 天", "145.5", "97/10000"]:
|
||||
if expected not in result_text:
|
||||
fail("预览金标准", f"结果缺少 {expected}:{result_text[:200]}")
|
||||
|
||||
except E2EFailure as exc:
|
||||
failures.append(str(exc))
|
||||
page.screenshot(path="e2e/failure.png")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
failures.append(f"未预期异常:{exc}")
|
||||
page.screenshot(path="e2e/failure.png")
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
if failures:
|
||||
print(f"✗ {failures[0]}")
|
||||
if console_errors:
|
||||
print(f" 控制台错误(最近 3 条):{console_errors[-3:]}")
|
||||
return 1
|
||||
|
||||
print("✓ 计提参数管理端到端:7 步全部通过(目录/待提交/提交/生效/版本/恢复/预览金标准)")
|
||||
if console_errors:
|
||||
print(f" (控制台非致命消息 {len(console_errors)} 条,见上方日志)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -738,7 +738,8 @@ onMounted(() => {
|
||||
<span>单笔绩效</span><b class="num">{{ previewResult.performance_amount }} 元</b>
|
||||
</div>
|
||||
<div class="pr-row"><span>回款天数</span><b class="num">{{ previewResult.payment_days }} 天</b></div>
|
||||
<div class="pr-row"><span>计提比例</span><b class="num">{{ previewResult.rate_text }}</b></div>
|
||||
<div class="pr-row"><span>计提比例</span><b class="num">{{ previewResult.rate }}</b></div>
|
||||
<div class="pr-row"><span>计提说明</span><b>{{ previewResult.rate_text }}</b></div>
|
||||
<div class="pr-row"><span>折算毛利润额</span><b class="num">{{ previewResult.converted_profit }} 元</b></div>
|
||||
<div class="pr-row"><span>毛利润率</span><b class="num">{{ previewResult.profit_rate }}</b></div>
|
||||
<div class="pr-row"><span>起付判定</span><b>{{ previewResult.start_payment_ok ? '已达标' : '未达标(累计中)' }}</b></div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user