diff --git a/.cursorrules b/.cursorrules index 06f3caf4..722c5336 100644 --- a/.cursorrules +++ b/.cursorrules @@ -19,7 +19,21 @@ - Vue 3 + Element Plus;主题与皮肤通过 **CSS 变量 + Element 主题变量 + 全局覆盖** 对齐规范,**勿随意改 Token 色值**。 - 树形主数据全路径:`web-admin/src/utils/tree-path.ts`(如 `buildTreePathLabelFromFlat`、`buildPathLabelMapFromTreeRoots`、`resolveTreePathDisplay`)。路径分隔符统一为 **` / `**(半角斜杠两侧各一空格)。 -- 参考实现:商品查看弹窗 `web-admin/src/components/business/product-view-dialog/index.vue`;列表与分页对齐 `#/base/unit` 等已改造页。 + +### 页面级 UI 模板(固定五页 + 工作台入口) + +上述页面均挂在侧栏 **「Demo」** 菜单下。新开发的列表/表单页**对照以下页面实现**,勿另起一套布局与类名习惯;仍须同时遵守 `docs/web-admin-ui-spec-fresh-mint.md`。 + +| 用途 | Hash 路由 | 源码 | +|------|-----------|------| +| 工作台(入口与说明) | `/#/demo/console` | `web-admin/src/views/dashboard/console/index.vue` | +| 标准列表(筛选、工具栏、表格、分页) | `/#/demo/unit` | `web-admin/src/views/system/unit/index.vue` | +| 复杂业务列表(状态、操作列等) | `/#/demo/order` | `web-admin/src/views/system/order/index.vue` | +| 商品类列表与查看弹窗联动 | `/#/demo/product` | `web-admin/src/views/system/product/index.vue` | +| 树形主数据 | `/#/demo/category` | `web-admin/src/views/system/category/index.vue` | +| 系统用户类 CRUD | `/#/demo/user` | `web-admin/src/views/system/user/index.vue` | + +商品**只读查看弹窗**结构对齐:`web-admin/src/components/business/product-view-dialog/index.vue`(由商品管理页打开,可与上表第三行一并参照)。 ### 视觉与 Token(与规范一致) @@ -48,7 +62,7 @@ - 工具栏:**「已选 N 项」在最前**,其后从左到右排列主按钮与批量操作;**禁止**用 `space-between` 把整组按钮推到行末。 - **同一行仅第一个主行动** 使用主色实心(`type="primary"` / `btn-main`);批量删除、导入导出等为中性描边。 - **操作列**:`fixed="right"`,预留足够宽度(建议 min-width ≥ 260px),避免按钮被裁切。 -- 主表:`list-table` + `stripe`;与已对齐订单列表一致时 **勿随意加 `border`**(以规范及参考页为准)。 +- 主表:`list-table` + `stripe`;与参考页「订单列表」`/#/demo/order` 一致时 **勿随意加 `border`**(以规范及上表模板页为准)。 - 分页:表格与 **`el-pagination`** 同放在 **`list-body.list-wrap`** 内;外层 **`.pagination.list-pager-bar`**,`margin-top: 0`,上边框与 padding 见规范第 7.9 节。 ### 树形选择器与查看态 diff --git a/backend/.env.example b/backend/.env.example index 2b583e14..6cdcdd18 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,16 +1,25 @@ -# 数据库连接配置示例 +# 数据库连接配置示例(V0.3 库名示例见 docs/sql/zkxw-requirements-v0.3-schema.sql) # DATABASE_URL 格式:mysql+pymysql://user:password@host:port/dbname DATABASE_URL=mysql+pymysql://root:password@localhost:3306/zkxw +# 与 sys_user.tenant_no 一致;登录按 (tenant_no, user_no) 查用户 +# DEFAULT_TENANT_NO=zkxw +# 用户无 sys_user_role 记录时,Web 管理端 JWT 使用的默认角色(super_admin 可全量菜单) +# WEB_AUTH_DEFAULT_ROLE=super_admin + # JWT配置示例 # SECRET_KEY=your-secret-key-change-in-production # ALGORITHM=HS256 # ACCESS_TOKEN_EXPIRE_MINUTES=30 # 应用配置示例 -# APP_NAME=中科兴皖一体化门户 +# APP_NAME=陇耘鲜达 # DEBUG=true +# 浏览器跨域:若设置 BACKEND_CORS_ORIGINS,须为 JSON 数组,会整体覆盖代码默认值,例如: +# BACKEND_CORS_ORIGINS=["http://localhost:5173","http://localhost:3006","http://127.0.0.1:3006"] +# DEBUG=true 时后端还会允许任意本机端口的 Origin(localhost / 127.0.0.1),便于本地换端口开发。 + # 登录 RSA(Web/小程序加密登录;多实例须配置同一私钥 PEM) # LOGIN_RSA_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" # 仅本地用 Swagger/Apifox 明文测登录时可临时打开(生产务必 false) diff --git a/backend/README.md b/backend/README.md index d55e35b1..d8b35612 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,8 @@ -# 中科兴皖一体化门户后端(空壳) +# zkxw 后端骨架(FastAPI) -当前 `app.main` **不连接数据库、不注册任何业务路由**,仅提供根路径与 `/health`,便于占位或 CI。原 `app/api` 下 Python 文件已删除(请从 git 历史恢复)。`app/models`、`app/crud` 等目录仍保留为未引用代码,可按需继续清理。 +基于 FastAPI 的精简后端:**Web 鉴权**(登录 RSA、登录、刷新 Token、登出、当前用户、菜单 code)、**静态运营菜单树**、**运营端路由占位**。原叁强鲜配业务 API 已移除,可按模块在 `app/api/admin.py` 等位置扩展。 + +数据库最小初始化见:`docs/sql/zkxw-backend-skeleton.sql`(默认账号 `admin` / `111111`,角色 `super_admin`)。 ## 技术栈 @@ -15,12 +17,19 @@ ``` backend/ ├── app/ -│ ├── api/ # 包占位(路由实现已删除) +│ ├── api/ # API 路由 +│ │ ├── admin/ # 运营端接口 +│ │ ├── supplier/ # 供应商端接口 +│ │ ├── client/ # 客户端接口 +│ │ └── deliver/ # 配送员端接口 │ ├── core/ # 核心配置 -│ ├── models/ # 数据库模型 -│ ├── schemas/ # Pydantic schemas -│ ├── services/ # 业务逻辑 -│ └── main.py # 应用入口 +│ │ ├── config.py # 应用配置 +│ │ ├── security.py # 安全认证 +│ │ └── database.py # 数据库连接 +│ ├── models/ # 数据库模型 +│ ├── schemas/ # Pydantic schemas +│ ├── services/ # 业务逻辑 +│ └── main.py # 应用入口 ├── requirements.txt └── README.md ``` @@ -47,7 +56,7 @@ pip install -r requirements.txt ```env # 数据库配置 -DATABASE_URL=mysql+pymysql://root:password@localhost:3306/zkxw +DATABASE_URL=mysql+pymysql://root:password@localhost:3306/sanqiang # JWT配置 SECRET_KEY=your-secret-key-change-in-production @@ -55,7 +64,7 @@ ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=30 # 应用配置 -APP_NAME=中科兴皖一体化门户 +APP_NAME=陇耘鲜达 DEBUG=true ``` diff --git a/backend/app.pid b/backend/app.pid index 4f4cd1ff..ca2ea278 100644 --- a/backend/app.pid +++ b/backend/app.pid @@ -1 +1 @@ -11632 +47224 diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 1f9cc0f0..0638f42a 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1 +1 @@ -# 中科兴皖一体化门户后端(骨架) +# 陇耘鲜达后端 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index c0127a03..e5083be4 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -1,3 +1,10 @@ -"""API 包:业务路由模块已删除,仅保留包占位。恢复历史实现请使用 git。""" +"""API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)""" +from app.api.public import router as public_router +from app.api.menu import router as menu_router +from app.api.admin import router as admin_router -__all__: list[str] = [] +__all__ = [ + "public_router", + "menu_router", + "admin_router", +] diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py new file mode 100644 index 00000000..39c24224 --- /dev/null +++ b/backend/app/api/admin.py @@ -0,0 +1,160 @@ +"""运营端管理接口(租户、角色下拉等)""" +from typing import Any, List, Optional + +from fastapi import APIRouter, Depends, Query +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.menu_guard import require_admin_or_super_admin +from app.core.response import success_response, error_response +from app.crud import tenant as tenant_svc +from app.models.models import SysRole +from app.services.web_auth_roles import _map_role_name_to_web + +router = APIRouter(prefix="/web/admin", tags=["运营端管理"]) + + +# --------------------------------------------------------------------------- +# 角色下拉(用户弹窗等) +# --------------------------------------------------------------------------- + + +@router.get("/roles") +async def list_roles_for_select( + _user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + """返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。""" + rows: List[SysRole] = ( + db.query(SysRole) + .filter(SysRole.del_flag == 0, SysRole.status == "1") + .order_by(SysRole.order_num.asc()) + .all() + ) + seen: set[str] = set() + out: List[dict] = [] + for r in rows: + name = (r.role_name or "").strip() + if not name: + continue + code = _map_role_name_to_web(name) + key = f"{code}:{name}" + if key in seen: + continue + seen.add(key) + out.append({"label": name, "value": code}) + if not out: + out = [ + {"label": "超级管理员", "value": "super_admin"}, + {"label": "运营管理员", "value": "admin"}, + ] + return success_response(out) + + +# --------------------------------------------------------------------------- +# 租户管理 +# --------------------------------------------------------------------------- + + +class TenantCreateBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + tenant_no: str = Field(..., alias="tenantNo") + tenant_name: str = Field(..., alias="tenantName") + contact_name: Optional[str] = Field(None, alias="contactName") + contact_phone: Optional[str] = Field(None, alias="contactPhone") + order_num: Optional[int] = Field(0, alias="orderNum") + remark: Optional[str] = None + + +class TenantUpdateBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + tenant_no: Optional[str] = Field(None, alias="tenantNo") + tenant_name: Optional[str] = Field(None, alias="tenantName") + contact_name: Optional[str] = Field(None, alias="contactName") + contact_phone: Optional[str] = Field(None, alias="contactPhone") + order_num: Optional[int] = Field(None, alias="orderNum") + remark: Optional[str] = None + + +@router.get("/tenants") +async def list_tenants( + page: int = Query(1, ge=1), + pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), + keyword: Optional[str] = None, + _user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + items, total = tenant_svc.list_tenants(db, page=page, page_size=pageSize, keyword=keyword) + total_pages = tenant_svc.total_pages(total, pageSize) + return success_response( + { + "list": items, + "total": total, + "page": page, + "pageSize": pageSize, + "totalPages": total_pages, + } + ) + + +@router.post("/tenants") +async def create_tenant( + body: TenantCreateBody, + user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + try: + obj = tenant_svc.create_tenant( + db, + tenant_no=body.tenant_no, + tenant_name=body.tenant_name, + contact_name=body.contact_name, + contact_phone=body.contact_phone, + order_num=body.order_num, + remark=body.remark, + user_id=user.get("user_id"), + ) + return success_response(tenant_svc._row_to_dict(obj), message="创建成功") + except ValueError as e: + return error_response(400, str(e)) + + +@router.put("/tenants/{tenant_id}") +async def update_tenant( + tenant_id: str, + body: TenantUpdateBody, + user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + try: + payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) + obj = tenant_svc.update_tenant( + db, + tenant_id, + tenant_no=payload.get("tenant_no"), + tenant_name=payload.get("tenant_name"), + contact_name=payload.get("contact_name"), + contact_phone=payload.get("contact_phone"), + order_num=payload.get("order_num"), + remark=payload.get("remark"), + user_id=user.get("user_id"), + ) + return success_response(tenant_svc._row_to_dict(obj), message="更新成功") + except ValueError as e: + return error_response(400, str(e)) + + +@router.delete("/tenants/{tenant_id}") +async def delete_tenant( + tenant_id: str, + _user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + try: + tenant_svc.soft_delete_tenant(db, tenant_id) + return success_response(None, message="删除成功") + except ValueError as e: + return error_response(400, str(e)) diff --git a/backend/app/api/menu.py b/backend/app/api/menu.py new file mode 100644 index 00000000..86bcfced --- /dev/null +++ b/backend/app/api/menu.py @@ -0,0 +1,107 @@ +"""运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)""" +from fastapi import APIRouter, Depends + +from app.core.menu_guard import require_admin_or_super_admin +from app.core.response import success_response + +router = APIRouter(prefix="/web/menu", tags=["菜单管理"]) + +# 与 web-admin/src/router/modules/sqxp.ts 一致:「Demo」参考页 +「系统管理」 +STATIC_MENU_TREE = [ + { + "id": "10000000000000000000000000000002", + "name": "Demo", + "path": "/demo", + "component": "/index/index", + "meta": { + "title": "Demo", + "icon": "ri:layout-grid-line", + "keepAlive": False, + "roles": ["admin"], + }, + "children": [ + { + "id": "10000000000000000000000000000001_child", + "name": "Console", + "path": "console", + "component": "/dashboard/console/index", + "meta": { + "title": "工作台", + "icon": "ri:home-smile-2-line", + "keepAlive": True, + "fixedTab": True, + "menuCode": "dashboard", + }, + }, + { + "id": "10000000000000000000000000000005", + "name": "Unit", + "path": "unit", + "component": "/system/unit/index", + "meta": {"title": "商品单位", "keepAlive": True}, + }, + { + "id": "10000000000000000000000000000021", + "name": "OrderList", + "path": "order", + "component": "/system/order/index", + "meta": {"title": "订单列表", "keepAlive": True}, + }, + { + "id": "10000000000000000000000000000006", + "name": "Product", + "path": "product", + "component": "/system/product/index", + "meta": {"title": "商品管理", "keepAlive": True}, + }, + { + "id": "10000000000000000000000000000004", + "name": "Category", + "path": "category", + "component": "/system/category/index", + "meta": {"title": "商品分类", "keepAlive": True}, + }, + { + "id": "10000000000000000000000000000007", + "name": "User", + "path": "user", + "component": "/system/user/index", + "meta": {"title": "用户管理", "keepAlive": True}, + }, + { + "id": "10000000000000000000000000000008", + "name": "UserDetail", + "path": "user/detail", + "component": "/system/user/detail", + "meta": {"title": "用户详情", "isHide": True, "keepAlive": True}, + }, + ], + }, + { + "id": "20000000000000000000000000000001", + "name": "System", + "path": "/system", + "component": "/index/index", + "meta": { + "title": "系统管理", + "icon": "ri:settings-3-line", + "keepAlive": False, + "roles": ["admin"], + }, + "children": [ + { + "id": "20000000000000000000000000000002", + "name": "Tenant", + "path": "tenant", + "component": "/system/tenant/index", + "meta": {"title": "租户管理", "keepAlive": True}, + }, + ], + }, +] + + +@router.get("/list") +async def get_menu_list(_user: dict = Depends(require_admin_or_super_admin)): + """获取菜单列表(与当前前端路由一致)""" + return success_response({"menuList": STATIC_MENU_TREE}) diff --git a/backend/app/api/public.py b/backend/app/api/public.py new file mode 100644 index 00000000..bb66764d --- /dev/null +++ b/backend/app/api/public.py @@ -0,0 +1,289 @@ +"""公共接口(Web 登录、刷新 Token、登出;用户模型对齐 zkxw-requirements-v0.3-schema.sql)""" +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Request +from sqlalchemy.exc import OperationalError +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.security import verify_password +from app.core.auth import ( + create_access_token, + create_refresh_token, + decode_token, + get_current_user_by_token, + extend_session, + is_refresh_token_valid, + is_session_valid, + revoke_refresh_token, + revoke_session, + store_refresh_token, + store_session, +) +from app.core.response import success_response, error_response +from app.models.models import SysUser +from app.crud.user import user_crud +from app.core.utils import generate_uuid +from app.core import login_crypto +from app.services.web_auth_roles import iter_user_menu_perm_codes, resolve_web_role_for_user + +router = APIRouter(prefix="", tags=["公共接口"]) + + +def _avatar_url_field(user: SysUser) -> Optional[str]: + raw = getattr(user, "avatar", None) + if not raw: + return None + s = str(raw).strip() + return s if s else None + + +def _build_user_info_payload(user: SysUser, web_role: str) -> dict: + """与 web-admin `userInfo` 字段约定一致。""" + phone = (getattr(user, "phone_no", None) or "").strip() or None + return { + "id": user.id, + "username": user.user_no, + "realName": user.nick_name, + "phone": phone, + "email": (getattr(user, "email", None) or "").strip() or None, + "role": web_role, + "regionId": user.dept_id, + "regionName": None, + "avatarUrl": _avatar_url_field(user), + } + + +def _jwt_user_payload(user: SysUser, web_role: str) -> dict: + return { + "sub": user.id, + "username": user.user_no, + "role": web_role, + "region_id": user.dept_id, + "real_name": user.nick_name, + } + + +@router.get("/web/auth/login-rsa-public-key", response_model=dict) +async def web_login_rsa_public_key(): + """Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致)""" + login_crypto.ensure_initialized() + return success_response({"publicKeyPem": login_crypto.get_public_pem()}) + + +@router.post("/web/auth/login", response_model=dict) +async def web_login( + request: Request, + body: dict, + db: Session = Depends(get_db), +): + """Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。""" + login_crypto.ensure_initialized() + user_no, password, cred_err = login_crypto.extract_login_credentials(body) + if cred_err: + return error_response(400, cred_err) + + user = user_crud.get_by_user_no(db, user_no) + + if not user: + return error_response(401, "用户名或密码错误") + + pwd_hash = getattr(user, "password", None) or "" + if not verify_password(password, pwd_hash): + return error_response(401, "用户名或密码错误") + + if user.status != "1": + return error_response(403, "账号已被停用,请联系管理员") + + web_role = resolve_web_role_for_user(db, user.id) + + session_id = generate_uuid() + access_jti = generate_uuid() + refresh_jti = generate_uuid() + + access_token = create_access_token( + data=_jwt_user_payload(user, web_role), + session_id=session_id, + token_jti=access_jti, + ) + refresh_token = create_refresh_token( + data={ + "sub": user.id, + "username": user.user_no, + "role": web_role, + }, + session_id=session_id, + refresh_jti=refresh_jti, + ) + + await store_session(session_id=session_id, user_id=user.id) + await store_refresh_token(refresh_jti=refresh_jti, user_id=user.id) + + return success_response( + { + "token": access_token, + "refreshToken": refresh_token, + "expiresIn": 7200, + "userInfo": _build_user_info_payload(user, web_role), + } + ) + + +@router.post("/web/auth/refresh-token", response_model=dict) +async def refresh_token(body: dict, db: Session = Depends(get_db)): + """刷新 Token""" + refresh_token_val = body.get("refreshToken") + if not refresh_token_val: + return error_response(400, "refreshToken不能为空") + + try: + payload = decode_token(refresh_token_val) + if payload.get("type") != "refresh": + return error_response(401, "无效的refreshToken") + + session_id = payload.get("sid") + refresh_jti = payload.get("jti") + + if not await is_session_valid(session_id=session_id): + return error_response(401, "refreshToken已失效,请重新登录") + if not await is_refresh_token_valid(refresh_jti=refresh_jti): + return error_response(401, "无效的refreshToken") + + user_id = payload.get("sub") + user = user_crud.get(db, user_id) + + if not user or user.del_flag != 0 or user.status != "1": + return error_response(401, "用户不存在或已禁用") + + web_role = resolve_web_role_for_user(db, user.id) + + await revoke_refresh_token(refresh_jti=refresh_jti) + await extend_session(session_id=session_id) + + new_access_jti = generate_uuid() + new_refresh_jti = generate_uuid() + + new_access_token = create_access_token( + data=_jwt_user_payload(user, web_role), + session_id=session_id, + token_jti=new_access_jti, + ) + new_refresh_token = create_refresh_token( + data={ + "sub": user.id, + "username": user.user_no, + "role": web_role, + }, + session_id=session_id, + refresh_jti=new_refresh_jti, + ) + + await store_refresh_token(refresh_jti=new_refresh_jti, user_id=user.id) + + return success_response( + { + "token": new_access_token, + "refreshToken": new_refresh_token, + "expiresIn": 7200, + } + ) + except HTTPException: + return error_response(401, "refreshToken已过期") + + +@router.post("/web/auth/logout", response_model=dict) +async def logout(request: Request): + """登出:吊销 Redis 会话(若已配置 REDIS_HOST)。""" + authorization = request.headers.get("Authorization", "") + if not authorization.startswith("Bearer "): + return error_response(401, "未授权") + + token = authorization.replace("Bearer ", "") + try: + payload = decode_token(token) + session_id = payload.get("sid") + if session_id: + await revoke_session(session_id=session_id) + except HTTPException: + return error_response(401, "Token无效或已过期") + + return success_response({"message": "登出成功"}) + + +@router.get("/web/auth/current-user", response_model=dict) +async def get_current_user_profile(request: Request, db: Session = Depends(get_db)): + """获取当前用户信息(与 sys_user 字段对应)。""" + authorization = request.headers.get("Authorization", "") + if not authorization.startswith("Bearer "): + return error_response(401, "未授权") + + token = authorization.replace("Bearer ", "") + try: + current_user = await get_current_user_by_token(token=token, db=db) + + au = current_user.get("avatar_url") + avatar_url = str(au).strip() if au else None + return success_response( + { + "id": current_user.get("user_id"), + "username": current_user.get("username"), + "realName": current_user.get("real_name"), + "phone": current_user.get("phone"), + "email": current_user.get("email"), + "role": current_user.get("role"), + "isSuperAdmin": current_user.get("role") == "super_admin", + "regionId": current_user.get("region_id"), + "regionName": None, + "regionIds": [], + "isActive": current_user.get("is_active"), + "avatarUrl": avatar_url if avatar_url else None, + } + ) + except HTTPException: + return error_response(401, "Token无效或已过期") + + +@router.get("/web/auth/menu-codes", response_model=dict) +async def get_web_auth_menu_codes(request: Request, db: Session = Depends(get_db)): + """获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。""" + authorization = request.headers.get("Authorization", "") + if not authorization.startswith("Bearer "): + return error_response(401, "未授权") + + token = authorization.replace("Bearer ", "") + try: + current_user = await get_current_user_by_token(token=token, db=db) + role = current_user.get("role") + if role == "super_admin": + return success_response( + { + "isSuperAdmin": True, + "all": True, + "menuCodes": [], + } + ) + + try: + codes = iter_user_menu_perm_codes(db, current_user.get("user_id") or "") + menu_codes = sorted(codes) + except OperationalError: + menu_codes = [] + + if role == "admin" and not menu_codes: + return success_response( + { + "isSuperAdmin": False, + "all": True, + "menuCodes": [], + } + ) + + return success_response( + { + "isSuperAdmin": False, + "all": False, + "menuCodes": menu_codes, + } + ) + except HTTPException: + return error_response(401, "Token无效或已过期") diff --git a/backend/app/core/api_crypto_middleware.py b/backend/app/core/api_crypto_middleware.py index 7ae3cb0d..892c3188 100644 --- a/backend/app/core/api_crypto_middleware.py +++ b/backend/app/core/api_crypto_middleware.py @@ -6,7 +6,7 @@ import json import logging import time from typing import Any, Optional, Tuple -from urllib.parse import urlencode +from urllib.parse import parse_qsl, urlencode import redis.asyncio as redis from jose import jwt @@ -102,10 +102,56 @@ def _json_error_response(code: int, message: str) -> Response: return Response(content=json.dumps(body, ensure_ascii=False), media_type="application/json", status_code=200) +async def _coerce_post_method_override_to_get(request: Request, call_next) -> Response: + """前端 Axios 在开启加密时会把 GET 改为 POST 并带 X-HTTP-Method-Override: GET。 + + 当后端未启用 API_ENCRYPT_ENABLED 时,原逻辑直接放行,路由仍按 POST 匹配,导致仅注册了 GET 的接口返回 405。 + 此处在不解密的情况下,将此类请求还原为 GET(空或明文 JSON 体);若仍为 enc=1 密文则提示配置不一致。 + """ + if request.method != "POST": + return await call_next(request) + override = (request.headers.get("x-http-method-override") or "").upper() + if override != "GET": + return await call_next(request) + + body_bytes = await request.body() + req_json: dict = {} + if body_bytes: + try: + raw = json.loads(body_bytes.decode("utf-8")) + req_json = raw if isinstance(raw, dict) else {} + except Exception: + req_json = {} + + if int(req_json.get("enc", 0) or 0) == 1: + return _json_error_response( + 400, + "客户端已开启接口加密(enc=1),但服务端未启用 API_ENCRYPT_ENABLED;" + "请在后端开启加密并配置 API_ENCRYPT_FIXED_SECRET,或将前端 VITE_API_ENCRYPT_ENABLED 设为 false。", + ) + + scope = dict(request.scope) + scope["method"] = "GET" + payload = req_json if isinstance(req_json, dict) else {} + q0 = (request.scope.get("query_string") or b"").decode("utf-8") + pairs = dict(parse_qsl(q0, keep_blank_values=True)) + for k, v in payload.items(): + if isinstance(v, (dict, list)): + continue + pairs[k] = "" if v is None else str(v) + scope["query_string"] = urlencode(pairs, doseq=True).encode("utf-8") if pairs else b"" + + async def receive(): + return {"type": "http.request", "body": b"", "more_body": False} + + new_request = Request(scope, receive) + return await call_next(new_request) + + class ApiCryptoMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): if not settings.API_ENCRYPT_ENABLED: - return await call_next(request) + return await _coerce_post_method_override_to_get(request, call_next) if not settings.API_ENCRYPT_FIXED_SECRET: if settings.API_ENCRYPT_FAIL_OPEN: logger.warning("api crypto bypassed: API_ENCRYPT_FIXED_SECRET is empty") diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py index f5e76455..b0e2129c 100644 --- a/backend/app/core/auth.py +++ b/backend/app/core/auth.py @@ -12,10 +12,9 @@ from sqlalchemy.orm import Session from app.core.config import settings from app.core.database import get_db -from app.core.security import verify_password -from app.models.models import SysUser from app.crud.user import user_crud from app.core.utils import generate_uuid +from app.services.web_auth_roles import resolve_web_role_for_user oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/public/login") @@ -197,27 +196,34 @@ async def get_current_user_by_token( ) user = user_crud.get(db, user_id) - if not user or user.is_del == 1: + if not user or getattr(user, "del_flag", 0) != 0: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在或已删除", ) - if not user.is_active: + if getattr(user, "status", "1") != "1": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户已被禁用", ) + web_role = resolve_web_role_for_user(db, user.id) + + phone = (getattr(user, "phone_no", None) or "").strip() or None + email = (getattr(user, "email", None) or "").strip() or None + avatar = getattr(user, "avatar", None) + return { "user_id": user.id, - "username": user.username, - "real_name": user.real_name, - "role": user.role, - "region_id": user.region_id, - "phone": getattr(user, "phone", None), - "avatar_url": getattr(user, "avatar_url", None), - "is_active": getattr(user, "is_active", True), + "username": user.user_no, + "real_name": user.nick_name, + "role": web_role, + "region_id": user.dept_id, + "phone": phone, + "email": email, + "avatar_url": avatar, + "is_active": user.status == "1", } diff --git a/backend/app/core/config.py b/backend/app/core/config.py index c5d33b90..d92c06d4 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -8,12 +8,12 @@ class Settings(BaseSettings): """应用配置""" # 应用基础配置 - APP_NAME: str = "中科兴皖一体化门户" + APP_NAME: str = "zkxw 管理骨架" DEBUG: bool = True API_V1_PREFIX: str = "/api" # 数据库配置 - DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/zkxw" + DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/sanqiang" # JWT配置 SECRET_KEY: str = "your-secret-key-change-in-production-please-use-random-string" @@ -100,6 +100,11 @@ class Settings(BaseSettings): WX_TEMPLATE_REFUND_RESULT: str = "" # 退款结果通知模板ID WX_TEMPLATE_DELIVERY_WARNING: str = "" # 配送时效预警模板ID + # 与 docs/sql/zkxw-requirements-v0.3-schema.sql 一致:登录按租户 + sys_user.user_no + DEFAULT_TENANT_NO: str = "zkxw" + # 用户未绑定 sys_user_role 时,Web 管理端默认角色(建议 super_admin 便于开发全菜单) + WEB_AUTH_DEFAULT_ROLE: str = "super_admin" + # 登录 RSA:私钥 PEM(多实例/负载均衡须配置同一私钥;留空则进程启动时临时生成,仅适合单机开发) LOGIN_RSA_PRIVATE_KEY_PEM: str = "" # 是否允许明文 username/password 登录(仅建议本地排障临时开启;生产务必 false) diff --git a/backend/app/core/menu_guard.py b/backend/app/core/menu_guard.py index 3a427b08..d32c8a25 100644 --- a/backend/app/core/menu_guard.py +++ b/backend/app/core/menu_guard.py @@ -1,4 +1,4 @@ -"""菜单 code 权限守卫""" +"""菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms)""" from typing import Callable, Set from fastapi import Depends, HTTPException @@ -6,7 +6,7 @@ from sqlalchemy.orm import Session from app.core.auth import get_current_user from app.core.database import get_db -from app.models.models import SysUserMenuScope +from app.services.web_auth_roles import iter_user_menu_perm_codes ADMIN_ROLES = {"admin", "super_admin"} SUPER_ADMIN_ROLE = "super_admin" @@ -21,18 +21,8 @@ def require_admin_or_super_admin(current_user: dict = Depends(get_current_user)) def get_user_menu_codes(db: Session, user_id: str) -> Set[str]: - """读取用户授权的菜单 code 集合。""" - if not user_id: - return set() - rows = ( - db.query(SysUserMenuScope.menu_code) - .filter( - SysUserMenuScope.user_id == user_id, - SysUserMenuScope.is_del == 0, - ) - .all() - ) - return {str(row[0]).strip() for row in rows if row and row[0]} + """读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。""" + return iter_user_menu_perm_codes(db, user_id or "") def require_menu_code(menu_code: str) -> Callable: diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py index ceb89e4b..e90b86e3 100644 --- a/backend/app/crud/__init__.py +++ b/backend/app/crud/__init__.py @@ -1,73 +1,9 @@ -"""CRUD模块""" +"""CRUD 模块""" from app.crud.base import CRUDBase, HistoryCRUD -from app.crud.region import region_crud -from app.crud.category import category_crud -from app.crud.unit import unit_crud from app.crud.user import user_crud -from app.crud.password_history import password_history_crud -from app.crud.product import product_crud -from app.crud.address import address_crud -from app.crud.inquiry import inquiry_crud, inquiry_item_crud -from app.crud.quotation import quotation_crud, quotation_item_crud -from app.crud.price_list import price_list_crud, price_list_item_crud -from app.crud.order import order_crud, order_item_crud, order_status_log_crud -from app.crud.delivery import delivery_task_crud, delivery_task_item_crud -from app.crud.settlement import settlement_crud, refund_crud -from app.crud.ord_settlement import ( - ord_client_settlement_crud, - ord_client_settlement_item_crud, - ord_supplier_settlement_crud, - ord_supplier_settlement_item_crud, -) -from app.crud.deliverer_review import deliverer_review_crud -from app.crud.product_banner import product_banner, product_banner_his -from app.crud.product_detail_media import product_detail_media, product_detail_media_his -from app.crud.product_parameter import product_parameter, product_parameter_his -from app.crud.category_parameter import category_parameter, category_parameter_his -from app.crud.product_parameter_value import product_parameter_value, product_parameter_value_his -from app.crud.product_review import product_review, product_review_his -from app.crud.product_review_media import product_review_media, product_review_media_his __all__ = [ "CRUDBase", "HistoryCRUD", - "region_crud", - "category_crud", - "unit_crud", "user_crud", - "password_history_crud", - "product_crud", - "address_crud", - "inquiry_crud", - "inquiry_item_crud", - "quotation_crud", - "quotation_item_crud", - "price_list_crud", - "price_list_item_crud", - "order_crud", - "order_item_crud", - "order_status_log_crud", - "delivery_task_crud", - "delivery_task_item_crud", - "settlement_crud", - "refund_crud", - "ord_client_settlement_crud", - "ord_client_settlement_item_crud", - "ord_supplier_settlement_crud", - "ord_supplier_settlement_item_crud", - "deliverer_review_crud", - "product_banner", - "product_banner_his", - "product_detail_media", - "product_detail_media_his", - "product_parameter", - "product_parameter_his", - "category_parameter", - "category_parameter_his", - "product_parameter_value", - "product_parameter_value_his", - "product_review", - "product_review_his", - "product_review_media", - "product_review_media_his", ] diff --git a/backend/app/crud/address.py b/backend/app/crud/address.py deleted file mode 100644 index 8c4241e3..00000000 --- a/backend/app/crud/address.py +++ /dev/null @@ -1,122 +0,0 @@ -"""地址CRUD""" -from typing import Optional, List, Tuple -from sqlalchemy.orm import Session -from sqlalchemy import and_ - -from app.models.models import BasAddress, BasAddressHis -from app.crud.base import CRUDBase - - -class AddressCRUD(CRUDBase[BasAddress]): - """地址CRUD操作""" - - def get_by_user(self, db: Session, user_id: str) -> List[BasAddress]: - """获取用户的所有地址""" - return db.query(BasAddress).filter( - and_(BasAddress.user_id == user_id, BasAddress.is_del == 0) - ).order_by(BasAddress.create_at.desc(), BasAddress.id.desc()).all() - - def get_by_user_paged( - self, db: Session, user_id: str, skip: int = 0, limit: int = 20 - ) -> Tuple[List[BasAddress], int]: - """分页获取用户地址(按创建时间倒序)""" - q = db.query(BasAddress).filter( - and_(BasAddress.user_id == user_id, BasAddress.is_del == 0) - ) - total = q.count() - rows = ( - q.order_by(BasAddress.create_at.desc(), BasAddress.id.desc()) - .offset(skip) - .limit(limit) - .all() - ) - return rows, total - - def get_by_user_and_type(self, db: Session, user_id: str, address_type: str) -> List[BasAddress]: - """获取用户指定类型的地址""" - return db.query(BasAddress).filter( - and_(BasAddress.user_id == user_id, BasAddress.address_type == address_type, BasAddress.is_del == 0) - ).order_by(BasAddress.create_at.desc(), BasAddress.id.desc()).all() - - def get_default(self, db: Session, user_id: str, address_type: str = None) -> Optional[BasAddress]: - """获取用户默认地址""" - query = db.query(BasAddress).filter( - and_(BasAddress.user_id == user_id, BasAddress.is_default == True, BasAddress.is_del == 0) - ) - if address_type: - query = query.filter(BasAddress.address_type == address_type) - return query.first() - - def set_default(self, db: Session, user_id: str, address_id: str) -> bool: - """设置默认地址""" - # 先取消所有默认地址 - db.query(BasAddress).filter( - and_(BasAddress.user_id == user_id, BasAddress.is_default == True) - ).update({"is_default": False}) - - # 设置新的默认地址 - address = self.get(db, address_id) - if address and address.user_id == user_id: - address.is_default = True - db.commit() - return True - return False - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BasAddress]: - """更新地址并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "user_id": original.user_id, - "address_type": original.address_type, - "contact_person": original.contact_person, - "contact_phone": original.contact_phone, - "province": original.province, - "city": original.city, - "district": original.district, - "detail_address": original.detail_address, - "is_default": original.is_default, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BasAddressHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, id: str) -> bool: - """删除地址并记录历史""" - original = self.get(db, id) - if not original: - return False - - his_data = { - "id": original.id, - "user_id": original.user_id, - "address_type": original.address_type, - "contact_person": original.contact_person, - "contact_phone": original.contact_phone, - "province": original.province, - "city": original.city, - "district": original.district, - "detail_address": original.detail_address, - "is_default": original.is_default, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BasAddressHis(**his_data) - db.add(his) - - return self.delete(db, id) - - -address_crud = AddressCRUD(BasAddress) diff --git a/backend/app/crud/attachment.py b/backend/app/crud/attachment.py deleted file mode 100644 index 5460f4b0..00000000 --- a/backend/app/crud/attachment.py +++ /dev/null @@ -1,58 +0,0 @@ -"""附件CRUD""" -from typing import Optional, List -from sqlalchemy.orm import Session -from sqlalchemy import and_ - -from app.models.models import BizAttachment -from app.crud.base import CRUDBase -from app.core.utils import generate_id - - -class AttachmentCRUD(CRUDBase[BizAttachment]): - """附件CRUD操作""" - - def get_by_business( - self, - db: Session, - business_type: str, - business_id: str - ) -> List[BizAttachment]: - """根据业务类型和业务ID获取附件列表""" - return db.query(self.model).filter( - and_( - self.model.business_type == business_type, - self.model.business_id == business_id, - self.model.is_del == 0 - ) - ).order_by(self.model.create_at.desc()).all() - - def create_attachment( - self, - db: Session, - business_type: str, - business_id: str, - file_name: str, - file_url: str, - file_size: int = None, - file_type: str = None, - upload_by: str = None, - upload_by_name: str = None - ) -> BizAttachment: - """创建附件记录""" - data = { - "id": generate_id(), - "business_type": business_type, - "business_id": business_id, - "file_name": file_name, - "file_url": file_url, - "file_size": file_size, - "file_type": file_type, - "upload_by": upload_by, - "upload_by_name": upload_by_name, - "create_by": upload_by, - "create_name": upload_by_name - } - return self.create(db, data) - - -attachment_crud = AttachmentCRUD(BizAttachment) diff --git a/backend/app/crud/category.py b/backend/app/crud/category.py deleted file mode 100644 index 8abfda7e..00000000 --- a/backend/app/crud/category.py +++ /dev/null @@ -1,112 +0,0 @@ -"""商品分类CRUD""" -from typing import Optional, List -from sqlalchemy.orm import Session -from sqlalchemy import and_ - -from app.models.models import BasProductCategory, BasProductCategoryHis -from app.crud.base import CRUDBase - - -class CategoryCRUD(CRUDBase[BasProductCategory]): - """商品分类CRUD操作""" - - def get_by_name(self, db: Session, name: str, parent_id: str = None) -> Optional[BasProductCategory]: - """根据名称获取分类""" - query = db.query(BasProductCategory).filter( - and_(BasProductCategory.name == name, BasProductCategory.is_del == 0) - ) - if parent_id: - query = query.filter(BasProductCategory.parent_id == parent_id) - else: - query = query.filter(BasProductCategory.parent_id.is_(None)) - return query.first() - - def get_active_all(self, db: Session) -> List[BasProductCategory]: - """获取所有启用的分类""" - return db.query(BasProductCategory).filter( - and_(BasProductCategory.is_del == 0, BasProductCategory.is_active == True) - ).order_by(BasProductCategory.sort_order.asc()).all() - - def get_children(self, db: Session, parent_id: str) -> List[BasProductCategory]: - """获取子分类""" - return db.query(BasProductCategory).filter( - and_(BasProductCategory.parent_id == parent_id, BasProductCategory.is_del == 0) - ).order_by(BasProductCategory.sort_order.asc()).all() - - def get_tree(self, db: Session) -> List[dict]: - """获取分类树""" - all_categories = self.get_active_all(db) - return self._build_tree(all_categories, None) - - def _build_tree(self, categories: List[BasProductCategory], parent_id: str = None) -> List[dict]: - """构建树形结构""" - tree = [] - for cat in categories: - if cat.parent_id == parent_id: - pc = getattr(cat, "price_coefficient", None) - node = { - "id": cat.id, - "name": cat.name, - "parentId": cat.parent_id, - "sortOrder": cat.sort_order, - "priceCoefficient": float(pc) if pc is not None else 0.0, - "isActive": cat.is_active, - "children": self._build_tree(categories, cat.id) - } - tree.append(node) - return tree - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BasProductCategory]: - """更新分类并记录历史""" - original = self.get(db, id) - if not original: - return None - - pc_hist = getattr(original, "price_coefficient", None) - his_data = { - "id": original.id, - "name": original.name, - "image_url": getattr(original, "image_url", None), - "parent_id": original.parent_id, - "sort_order": original.sort_order, - "price_coefficient": pc_hist if pc_hist is not None else 0, - "is_active": original.is_active, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BasProductCategoryHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, id: str) -> bool: - """删除分类并记录历史""" - original = self.get(db, id) - if not original: - return False - - pc_hist = getattr(original, "price_coefficient", None) - his_data = { - "id": original.id, - "name": original.name, - "image_url": getattr(original, "image_url", None), - "parent_id": original.parent_id, - "sort_order": original.sort_order, - "price_coefficient": pc_hist if pc_hist is not None else 0, - "is_active": original.is_active, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BasProductCategoryHis(**his_data) - db.add(his) - - return self.delete(db, id) - - -category_crud = CategoryCRUD(BasProductCategory) diff --git a/backend/app/crud/category_parameter.py b/backend/app/crud/category_parameter.py deleted file mode 100644 index 2f0afb93..00000000 --- a/backend/app/crud/category_parameter.py +++ /dev/null @@ -1,116 +0,0 @@ -"""CRUD - 分类参数关联""" -from typing import List -from sqlalchemy.orm import Session -from sqlalchemy import and_ - -from app.crud.base import CRUDBase, HistoryCRUD -from app.models.category_parameter import BasCategoryParameter, BasCategoryParameterHis -from app.schemas.category_parameter import CategoryParameterBase - - -class CategoryParameterCRUD(CRUDBase[BasCategoryParameter]): - """分类参数关联CRUD操作""" - - def get_by_category(self, db: Session, category_id: str) -> List[BasCategoryParameter]: - """获取分类的所有关联参数""" - return db.query(self.model).filter( - self.model.category_id == category_id, - self.model.is_del == 0 - ).order_by(self.model.sort_order.asc()).all() - - def get_by_category_with_detail(self, db: Session, category_id: str) -> List[dict]: - """获取分类的所有关联参数(带参数详情)""" - from app.models.product_parameter import BasProductParameter - - results = db.query( - BasCategoryParameter, - BasProductParameter - ).join( - BasProductParameter, - and_( - BasCategoryParameter.parameter_id == BasProductParameter.id, - BasProductParameter.is_del == 0 - ) - ).filter( - BasCategoryParameter.category_id == category_id, - BasCategoryParameter.is_del == 0 - ).order_by( - BasCategoryParameter.sort_order.asc() - ).all() - - return [ - { - "id": cp.id, - "parameterId": cp.parameter_id, - "parameterName": pp.name, - "parameterCode": pp.code, - "paramType": pp.param_type, - "options": pp.options, - "placeholder": pp.placeholder, - "isRequired": cp.is_required, - "defaultValue": cp.default_value, - "sortOrder": cp.sort_order, - } - for cp, pp in results - ] - - def batch_save( - self, - db: Session, - category_id: str, - items: List[dict], - creator: dict = None - ) -> List[BasCategoryParameter]: - """批量保存分类参数关联""" - # 先获取要删除的记录ID - old_records = db.query(self.model).filter( - self.model.category_id == category_id, - self.model.is_del == 0 - ).all() - - # 物理删除旧记录(释放唯一索引约束) - for record in old_records: - db.delete(record) - db.commit() - - # 创建新的关联 - saved_items = [] - for idx, item in enumerate(items): - item_data = { - "category_id": category_id, - "parameter_id": item.get("parameter_id"), - "is_required": item.get("is_required", 0), - "default_value": item.get("default_value"), - "sort_order": item.get("sort_order", idx), - } - cp = self.create(db, item_data, creator) - saved_items.append(cp) - - db.commit() - return saved_items - - def delete_by_category(self, db: Session, category_id: str) -> int: - """删除分类的所有关联""" - count = db.query(self.model).filter( - self.model.category_id == category_id, - self.model.is_del == 0 - ).update({"is_del": 1}) - db.commit() - return count - - def exists_link(self, db: Session, category_id: str, parameter_id: str) -> bool: - """检查是否已有关联""" - return db.query(self.model).filter( - self.model.category_id == category_id, - self.model.parameter_id == parameter_id, - self.model.is_del == 0 - ).first() is not None - - -class CategoryParameterHisCRUD(HistoryCRUD[BasCategoryParameterHis]): - """分类参数关联历史CRUD操作""" - pass - - -category_parameter = CategoryParameterCRUD(BasCategoryParameter) -category_parameter_his = CategoryParameterHisCRUD(BasCategoryParameterHis) diff --git a/backend/app/crud/client_cart.py b/backend/app/crud/client_cart.py deleted file mode 100644 index db69ebf1..00000000 --- a/backend/app/crud/client_cart.py +++ /dev/null @@ -1,280 +0,0 @@ -"""客户购物车 CRUD""" -from decimal import Decimal -from typing import List, Optional, Dict, Any -from collections import defaultdict -from datetime import datetime -from sqlalchemy.orm import Session -from sqlalchemy import and_ - -from app.models.models import BizClientCart, BizClientCartHis -from app.core.utils import generate_id -from app.crud.price_list import price_list_crud, price_list_item_crud -from app.crud.unit import unit_crud -from app.crud.product import product_crud -from app.crud.category import category_crud - - -class ClientCartCRUD: - def list_by_client(self, db: Session, client_id: str) -> List[BizClientCart]: - return ( - db.query(BizClientCart) - .filter(and_(BizClientCart.client_id == client_id, BizClientCart.is_del == 0)) - .order_by(BizClientCart.create_at.desc()) - .all() - ) - - def sync_to_active_price_list( - self, - db: Session, - client_id: str, - region_id: str, - operator: dict, - rows: Optional[List[BizClientCart]] = None, - ) -> bool: - """ - 将购物车中仍属于当前生效价格清单的商品行,同步为区域最新生效清单的单价、单位与清单 ID。 - 同一商品多条行(历史清单 ID 不同)合并数量并保留一行。 - 无生效清单时返回 False,不修改数据。 - """ - pl = price_list_crud.get_active(db, region_id) - if not pl: - return False - - rows = rows if rows is not None else self.list_by_client(db, client_id) - if not rows: - return True - - # 快速路径:如果购物车所有行已经属于当前生效价格清单,则无需做任何同步。 - # 这能避免在“频繁加购/改数量”场景下对整车做 update + his insert,导致卡顿。 - if all(getattr(r, "price_list_id", None) == pl.id for r in rows): - return True - - price_items = price_list_item_crud.get_by_price_list(db, pl.id) - pmap: Dict[str, Any] = {i.product_id: i for i in price_items} - - by_product: Dict[str, List[BizClientCart]] = defaultdict(list) - for r in rows: - if r.product_id in pmap: - by_product[r.product_id].append(r) - - op_uid = operator.get("user_id") - now = datetime.now() - - changed = False - - for product_id, rel in by_product.items(): - pi = pmap[product_id] - total_qty = sum(Decimal(str(r.quantity)) for r in rel) - if total_qty <= 0: - continue - new_unit_price = Decimal(str(pi.price)) - new_amount = (total_qty * new_unit_price).quantize(Decimal("0.01")) - keep = next((r for r in rel if r.price_list_id == pl.id), rel[0]) - - for r in rel: - if r.id != keep.id: - self._copy_to_his(db, r) - r.is_del = 1 - r.update_by = op_uid - r.update_at = now - changed = True - - # 只有发生实际变化时才写历史并更新主表,避免“重复同步”导致的历史膨胀与写放大 - product = product_crud.get(db, product_id) - unit = unit_crud.get(db, pi.unit_id) if pi.unit_id else None - cat_name = keep.category_name or "" - if product and product.category_id: - cat = category_crud.get(db, product.category_id) - cat_name = cat.name if cat else cat_name - - need_update = ( - keep.price_list_id != pl.id - or keep.unit_id != pi.unit_id - or Decimal(str(keep.quantity)) != total_qty - or Decimal(str(keep.unit_price)) != new_unit_price - or Decimal(str(keep.amount)) != new_amount - or (product and (keep.product_name != product.name or keep.image_url != product.image_url)) - or keep.unit_name != (unit.name if unit else keep.unit_name) - or keep.category_name != (cat_name or keep.category_name) - ) - if need_update: - self._copy_to_his(db, keep) - keep.price_list_id = pl.id - keep.unit_id = pi.unit_id - keep.quantity = total_qty - keep.unit_price = new_unit_price - keep.amount = new_amount - keep.product_name = product.name if product else keep.product_name - keep.unit_name = unit.name if unit else keep.unit_name - keep.category_name = cat_name or "" - keep.image_url = product.image_url if product else keep.image_url - keep.update_by = op_uid - keep.update_at = now - changed = True - - if changed: - db.commit() - return True - - def get(self, db: Session, cart_id: str) -> Optional[BizClientCart]: - return ( - db.query(BizClientCart) - .filter(and_(BizClientCart.id == cart_id, BizClientCart.is_del == 0)) - .first() - ) - - def _copy_to_his(self, db: Session, row: BizClientCart) -> None: - his = BizClientCartHis( - id=row.id, - client_id=row.client_id, - price_list_id=row.price_list_id, - product_id=row.product_id, - unit_id=row.unit_id, - quantity=row.quantity, - unit_price=row.unit_price, - amount=row.amount, - product_name=row.product_name, - unit_name=row.unit_name, - category_name=row.category_name, - image_url=row.image_url, - create_at=row.create_at, - create_by=row.create_by, - create_name=row.create_name, - update_at=row.update_at, - update_by=row.update_by, - his_create_at=datetime.now(), - ) - db.add(his) - - def add_or_update( - self, - db: Session, - client_id: str, - price_list_id: str, - product_id: str, - unit_id: str, - quantity: Decimal, - unit_price: Decimal, - product_name: str, - unit_name: str, - category_name: str, - image_url: str, - operator: dict, - ) -> BizClientCart: - matched = ( - db.query(BizClientCart) - .filter( - and_( - BizClientCart.client_id == client_id, - BizClientCart.product_id == product_id, - BizClientCart.price_list_id == price_list_id, - BizClientCart.is_del == 0, - ) - ) - .all() - ) - op_uid = operator.get("user_id") - now = datetime.now() - if matched: - # 兼容历史脏数据:同客户+同价目表+同商品可能存在多行,先合并再做增量更新。 - keep = matched[0] - merged_qty = sum(Decimal(str(r.quantity or 0)) for r in matched) - if merged_qty < 0: - merged_qty = Decimal("0") - - for row in matched[1:]: - self._copy_to_his(db, row) - row.is_del = 1 - row.update_by = op_uid - row.update_at = now - - self._copy_to_his(db, keep) - next_qty = merged_qty + Decimal(str(quantity)) - amount = (next_qty * unit_price).quantize(Decimal("0.01")) - keep.quantity = next_qty - keep.unit_price = unit_price - keep.amount = amount - keep.product_name = product_name - keep.unit_name = unit_name - keep.category_name = category_name - keep.image_url = image_url - keep.update_by = op_uid - keep.update_at = now - db.commit() - return keep - amount = (quantity * unit_price).quantize(Decimal("0.01")) - row = BizClientCart( - id=generate_id(), - client_id=client_id, - price_list_id=price_list_id, - product_id=product_id, - unit_id=unit_id, - quantity=quantity, - unit_price=unit_price, - amount=amount, - product_name=product_name, - unit_name=unit_name, - category_name=category_name or "", - image_url=image_url, - is_del=0, - create_by=op_uid, - create_name=operator.get("real_name"), - update_by=op_uid, - ) - db.add(row) - db.commit() - return row - - def update_quantity( - self, - db: Session, - cart_id: str, - client_id: str, - quantity: Decimal, - operator: dict, - current_row: Optional[BizClientCart] = None, - ) -> Optional[BizClientCart]: - row = current_row or self.get(db, cart_id) - if not row or row.client_id != client_id: - return None - self._copy_to_his(db, row) - row.quantity = quantity - row.amount = (quantity * row.unit_price).quantize(Decimal("0.01")) - row.update_by = operator.get("user_id") - row.update_at = datetime.now() - db.commit() - return row - - def soft_delete(self, db: Session, cart_id: str, client_id: str, operator: dict) -> bool: - row = self.get(db, cart_id) - if not row or row.client_id != client_id: - return False - self._copy_to_his(db, row) - row.is_del = 1 - row.update_by = operator.get("user_id") - row.update_at = datetime.now() - db.commit() - return True - - def clear(self, db: Session, client_id: str, operator: dict) -> int: - rows = self.list_by_client(db, client_id) - n = 0 - for row in rows: - self._copy_to_his(db, row) - row.is_del = 1 - row.update_by = operator.get("user_id") - row.update_at = datetime.now() - n += 1 - if n: - db.commit() - return n - - def batch_delete(self, db: Session, client_id: str, cart_ids: List[str], operator: dict) -> int: - n = 0 - for cid in cart_ids: - if self.soft_delete(db, cid, client_id, operator): - n += 1 - return n - - -client_cart_crud = ClientCartCRUD() diff --git a/backend/app/crud/deliverer_review.py b/backend/app/crud/deliverer_review.py deleted file mode 100644 index 4db8fee7..00000000 --- a/backend/app/crud/deliverer_review.py +++ /dev/null @@ -1,66 +0,0 @@ -"""配送员评价 CRUD""" -from typing import Optional, List -from sqlalchemy.orm import Session -from sqlalchemy import and_, func - -from app.crud.base import CRUDBase -from app.core.utils import generate_id -from app.models.models import BizDelivererReview - - -class DelivererReviewCRUD(CRUDBase[BizDelivererReview]): - """配送员评价 CRUD""" - - def __init__(self): - super().__init__(BizDelivererReview) - - def get_by_order(self, db: Session, order_id: str) -> Optional[BizDelivererReview]: - return db.query(self.model).filter( - and_( - self.model.order_id == order_id, - self.model.is_del == 0 - ) - ).first() - - def create_review( - self, - db: Session, - order_id: str, - task_id: Optional[str], - client_id: str, - deliverer_id: str, - score: int, - tags: Optional[List[str]] = None, - content: Optional[str] = None, - is_anonymous: bool = False, - creator: Optional[dict] = None - ) -> BizDelivererReview: - data = { - "id": generate_id(), - "order_id": order_id, - "task_id": task_id, - "client_id": client_id, - "deliverer_id": deliverer_id, - "score": score, - "tags": ",".join(tags) if tags else None, - "content": content, - "is_anonymous": is_anonymous, - "create_by": creator.get("user_id") if creator else None, - "create_name": creator.get("real_name") if creator else None - } - return self.create(db, data) - - def get_deliverer_rating_stats(self, db: Session, deliverer_id: str) -> dict: - total = db.query(func.count(self.model.id)).filter( - and_(self.model.deliverer_id == deliverer_id, self.model.is_del == 0) - ).scalar() or 0 - avg_score = db.query(func.avg(self.model.score)).filter( - and_(self.model.deliverer_id == deliverer_id, self.model.is_del == 0) - ).scalar() - return { - "total": int(total), - "avgScore": round(float(avg_score), 2) if avg_score is not None else 0.0 - } - - -deliverer_review_crud = DelivererReviewCRUD() diff --git a/backend/app/crud/delivery.py b/backend/app/crud/delivery.py deleted file mode 100644 index 99a1dfb4..00000000 --- a/backend/app/crud/delivery.py +++ /dev/null @@ -1,872 +0,0 @@ -"""配送任务CRUD""" -import json -from typing import Optional, List -from datetime import datetime, timedelta -from decimal import Decimal -from sqlalchemy.orm import Session -from sqlalchemy import and_, func, or_, select, exists - -from app.models.models import BizDeliveryTask, BizDeliveryTaskItem, BizDeliveryTaskHis, BizOrder -from app.crud.base import CRUDBase -from app.core.utils import generate_id - - -class DeliveryTaskCRUD(CRUDBase[BizDeliveryTask]): - """配送任务CRUD操作""" - - @staticmethod - def _sync_legacy_status_from_dual(task: BizDeliveryTask) -> None: - """根据双状态回写兼容字段 status。""" - ds = (getattr(task, "deliverer_status", None) or "").strip() - ss = (getattr(task, "supplier_prepare_status", None) or "").strip() - if ds == "delivered": - task.status = "completed" - elif ds == "delivering": - task.status = "delivering" - elif ss == "stocking": - task.status = "supplier_stocking" - elif ss == "prepared": - task.status = "supplier_ready" - else: - task.status = "assigned" if task.deliver_id else "pending" - - def get_by_order(self, db: Session, order_id: str) -> Optional[BizDeliveryTask]: - """根据订单ID获取配送任务""" - return db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.order_id == order_id, - BizDeliveryTask.is_del == 0 - ) - ).first() - - def get_multi_with_filters( - self, - db: Session, - region_id: str = None, - supplier_id: str = None, - status: str = None, - supplier_prepare_status: str = None, - order_no: str = None, - deliver_id: str = None, - keyword: str = None, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizDeliveryTask], int]: - """多条件查询配送任务""" - query = db.query(BizDeliveryTask).filter(BizDeliveryTask.is_del == 0) - - if region_id: - query = query.filter(BizDeliveryTask.region_id == region_id) - if supplier_id: - query = query.filter(BizDeliveryTask.supplier_id == supplier_id) - if supplier_prepare_status: - parts = [s.strip() for s in supplier_prepare_status.split(",") if s.strip()] - if parts: - if len(parts) > 1: - query = query.filter(BizDeliveryTask.supplier_prepare_status.in_(parts)) - else: - p0 = parts[0] - if p0 == "pending_prepare": - query = query.filter( - or_( - BizDeliveryTask.supplier_prepare_status == "pending_prepare", - BizDeliveryTask.supplier_prepare_status.is_(None), - ) - ) - else: - query = query.filter(BizDeliveryTask.supplier_prepare_status == p0) - elif status: - parts = [s.strip() for s in status.split(",") if s.strip()] - if parts: - if len(parts) > 1: - query = query.filter(BizDeliveryTask.status.in_(parts)) - else: - query = query.filter(BizDeliveryTask.status == parts[0]) - if order_no: - query = query.filter(BizDeliveryTask.task_no.like(f"%{order_no}%")) - if deliver_id: - query = query.filter(BizDeliveryTask.deliver_id == deliver_id) - if keyword: - kw = (keyword or "").strip() - if kw: - pattern = f"%{kw}%" - order_match = exists( - select(1).select_from(BizOrder).where( - and_( - BizOrder.id == BizDeliveryTask.order_id, - BizOrder.is_del == 0, - or_( - BizOrder.order_no.like(pattern), - BizOrder.contact_person.like(pattern), - BizOrder.contact_phone.like(pattern), - ), - ) - ) - ) - query = query.filter( - or_( - BizDeliveryTask.task_no.like(pattern), - BizDeliveryTask.order_id.like(pattern), - order_match, - ) - ) - - total = query.count() - tasks = query.order_by(BizDeliveryTask.create_at.desc()).offset(skip).limit(limit).all() - return tasks, total - - def get_by_deliver( - self, - db: Session, - deliver_id: str, - status: str = None, - deliverer_status: str = None, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizDeliveryTask], int]: - """根据配送员获取配送任务。优先 deliverer_status(小程序);否则按兼容字段 status。""" - query = db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.deliver_id == deliver_id, - BizDeliveryTask.is_del == 0, - BizDeliveryTask.status != "cancelled", - ) - ) - - if deliverer_status: - parts = [s.strip() for s in deliverer_status.split(",") if s.strip()] - if parts: - if len(parts) > 1: - query = query.filter(BizDeliveryTask.deliverer_status.in_(parts)) - elif parts[0] == "pending_pickup": - query = query.filter( - or_( - BizDeliveryTask.deliverer_status == "pending_pickup", - BizDeliveryTask.deliverer_status.is_(None), - ) - ) - else: - query = query.filter(BizDeliveryTask.deliverer_status == parts[0]) - elif status: - parts = [s.strip() for s in status.split(",") if s.strip()] - if parts: - if len(parts) > 1: - query = query.filter(BizDeliveryTask.status.in_(parts)) - else: - query = query.filter(BizDeliveryTask.status == parts[0]) - - total = query.count() - tasks = query.order_by(BizDeliveryTask.create_at.desc()).offset(skip).limit(limit).all() - return tasks, total - - def get_by_supplier( - self, - db: Session, - supplier_id: str, - status: str = None, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizDeliveryTask], int]: - """根据供应商获取配送任务""" - query = db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.supplier_id == supplier_id, - BizDeliveryTask.is_del == 0 - ) - ) - - if status: - query = query.filter(BizDeliveryTask.status == status) - - total = query.count() - tasks = query.order_by(BizDeliveryTask.create_at.desc()).offset(skip).limit(limit).all() - return tasks, total - - def get_pending_list( - self, - db: Session, - region_id: str = None, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizDeliveryTask], int]: - """获取待分配配送的任务""" - query = db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.status == "pending", - BizDeliveryTask.is_del == 0 - ) - ) - - if region_id: - query = query.filter(BizDeliveryTask.region_id == region_id) - - total = query.count() - tasks = query.order_by(BizDeliveryTask.create_at.asc()).offset(skip).limit(limit).all() - return tasks, total - - def assign_deliver( - self, - db: Session, - order_id: str, - deliver_id: str, - operator_id: str = None - ) -> Optional[BizDeliveryTask]: - """分配配送员""" - task = self.get_by_order(db, order_id) - if not task: - return None - - if task.status != "pending": - return None - - task.deliver_id = deliver_id - task.deliverer_status = "pending_pickup" - task.supplier_prepare_status = "pending_prepare" - self._sync_legacy_status_from_dual(task) - task.update_at = datetime.now() - - db.commit() - db.refresh(task) - return task - - def assign_deliver_batch( - self, - db: Session, - assignments: List[dict] - ) -> List[BizDeliveryTask]: - """批量分配配送员""" - results = [] - for assignment in assignments: - order_id = assignment.get("orderId") - deliver_id = assignment.get("deliverId") - task = self.assign_deliver(db, order_id, deliver_id) - if task: - results.append(task) - return results - - def update_status( - self, - db: Session, - task_id: str, - new_status: str, - operator_id: str = None, - remark: str = None, - image_urls: List[str] = None, - operator_real_name: str = None, - operator_role: str = "deliver", - ) -> Optional[BizDeliveryTask]: - """更新配送任务状态""" - task = self.get(db, task_id) - if not task: - return None - - old_status = task.status - task.status = new_status - task.update_at = datetime.now() - - if new_status == "picking": - # 配送员已取货:进入配送中(勿再保留 pending_pickup,否则列表/标题仍显示「待取货」) - task.deliverer_status = "delivering" - task.supplier_prepare_status = "picked_up" - task.pickup_time = datetime.now() - if remark: - task.pickup_remark = remark - if image_urls: - task.pickup_image_url = ",".join(image_urls) if len(image_urls) > 0 else None - elif new_status == "supplier_stocking": - task.supplier_prepare_status = "stocking" - elif new_status == "supplier_ready": - task.supplier_prepare_status = "prepared" - # 备货完成凭证(与配送员取货凭证共用字段,时间线区分为供应商操作) - if remark: - task.pickup_remark = remark - if image_urls: - task.pickup_image_url = ",".join(image_urls) if len(image_urls) > 0 else None - elif new_status == "delivering": - task.deliverer_status = "delivering" - if remark: - task.delivery_remark = remark - if image_urls: - task.delivery_image_url = ",".join(image_urls) if len(image_urls) > 0 else None - - elif new_status == "completed": - task.deliverer_status = "delivered" - # 送达确认时间:与模型注释一致;小程序取货后直达 completed,必须在此写入 - task.delivery_time = datetime.now() - if remark: - task.delivery_remark = remark - if image_urls: - task.delivery_image_url = ",".join(image_urls) if len(image_urls) > 0 else None - - self._sync_legacy_status_from_dual(task) - - # 写入订单「操作记录」:legacy task.status 若已为 picking,会与取货入参 new_status 相同而漏记; - # 配送员确认取货时必须仍写入一条。 - if task.order_id: - from app.crud.order import order_crud - - should_log_order = (old_status != new_status) or new_status == "picking" - if should_log_order: - order_crud._create_status_log( - db, - task.order_id, - f"dt_{old_status}" if old_status is not None else "", - f"dt_{new_status}", - operator_id or "system", - operator_role or "deliver", - reason=remark, - create_name=(operator_real_name or "").strip() or None, - ) - - db.commit() - db.refresh(task) - - if new_status == "delivering": - from app.crud.delivery_warning_resolve import auto_resolve_open_warnings_for_task - - auto_resolve_open_warnings_for_task(db, task_id) - - return task - - def cancel_task( - self, - db: Session, - task_id: str, - reason: str = None, - operator_id: str = None, - operator_real_name: str = None, - ) -> Optional[BizDeliveryTask]: - """取消配送任务""" - task = self.get(db, task_id) - if not task: - return None - - if task.status in ["completed", "cancelled"]: - return None - - old_status = task.status - task.status = "cancelled" - task.update_at = datetime.now() - - if task.order_id: - from app.crud.order import order_crud - - order_crud._create_status_log( - db, - task.order_id, - f"dt_{old_status}", - "dt_cancelled", - operator_id or "system", - "admin", - reason=reason, - create_name=(operator_real_name or "").strip() or None, - ) - - db.commit() - db.refresh(task) - - from app.crud.delivery_warning_resolve import auto_resolve_open_warnings_for_task - - auto_resolve_open_warnings_for_task(db, task_id) - - return task - - def reassign( - self, - db: Session, - task_id: str, - new_deliver_id: str, - operator_id: str = None, - operator_real_name: str = None, - ) -> Optional[BizDeliveryTask]: - """重新分配配送员""" - from app.crud.order import order_crud - from app.crud.user import user_crud - - task = self.get(db, task_id) - if not task: - return None - - if task.status in ["completed", "cancelled"]: - return None - - old_deliver_id = task.deliver_id - if old_deliver_id == new_deliver_id: - return task - - updater = None - if operator_id: - updater = {"user_id": operator_id, "real_name": operator_real_name} - - self.update_with_history( - db, - task_id, - {"deliver_id": new_deliver_id}, - updater=updater, - ) - task = self.get(db, task_id) - if not task: - return None - - def _dn(uid: Optional[str]) -> Optional[str]: - if not uid: - return None - u = user_crud.get(db, uid) - return (u.real_name if u else None) or uid - - nu = user_crud.get(db, new_deliver_id) - order_crud._create_status_log( - db, - task.order_id, - "", - "delivery.reassign", - operator_id or "system", - "admin", - reason=json.dumps( - { - "fromDeliverId": old_deliver_id, - "toDeliverId": new_deliver_id, - "fromDeliverName": _dn(old_deliver_id), - "toDeliverName": nu.real_name if nu else new_deliver_id, - }, - ensure_ascii=False, - ), - create_name=(operator_real_name or "").strip() or None, - ) - db.commit() - db.refresh(task) - return task - - def get_warning_tasks( - self, - db: Session, - region_id: str = None, - within_minutes: int = 60, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizDeliveryTask], int, int]: - """获取时效预警任务""" - now = datetime.now() - expire_time_threshold = now + timedelta(minutes=within_minutes) - - query = db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.status.in_(["pending", "assigned", "picking"]), - BizDeliveryTask.is_del == 0, - BizDeliveryTask.effective_time <= expire_time_threshold - ) - ) - - if region_id: - query = query.filter(BizDeliveryTask.region_id == region_id) - - total = query.count() - warning_count = query.filter( - BizDeliveryTask.effective_time <= now + timedelta(minutes=30) - ).count() - - tasks = query.order_by(BizDeliveryTask.effective_time.asc()).offset(skip).limit(limit).all() - return tasks, total, warning_count - - def _generate_task_no(self, db: Session) -> str: - """生成配送单号""" - today = datetime.now().strftime("%Y%m%d") - prefix = f"DLV{today}" - - last_task = db.query(BizDeliveryTask).filter( - BizDeliveryTask.task_no.like(f"{prefix}%") - ).order_by(BizDeliveryTask.task_no.desc()).first() - - if last_task: - last_no = int(last_task.task_no[-4:]) - new_no = last_no + 1 - else: - new_no = 1 - - return f"{prefix}{str(new_no).zfill(4)}" - - def create_from_order( - self, - db: Session, - order_id: str, - supplier_id: str, - region_id: str, - creator: dict = None - ) -> BizDeliveryTask: - """从订单创建配送任务""" - task_no = self._generate_task_no(db) - - # 设置配送截止时间(默认4小时) - effective_time = datetime.now() + timedelta(hours=4) - - task = BizDeliveryTask( - id=generate_id(), - task_no=task_no, - order_id=order_id, - region_id=region_id, - supplier_id=supplier_id, - status="pending", - deliverer_status="pending_pickup", - supplier_prepare_status="pending_prepare", - effective_time=effective_time, - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(task) - db.flush() - - return task - - def create_delivery_task_from_order( - self, - db: Session, - order_id: str, - order_items: List[dict], - supplier_id: str, - region_id: str, - creator: dict = None - ) -> tuple: - """从订单创建完整的配送任务(包含明细)""" - from app.crud.order import order_item_crud - - # 创建配送任务 - task = self.create_from_order( - db, order_id, supplier_id, region_id, creator - ) - - # 创建配送任务明细 - task_items = [] - for item_data in order_items: - # 获取订单项信息 - order_item = order_item_crud.get(db, item_data.get("order_item_id")) - if not order_item: - continue - - task_item = BizDeliveryTaskItem( - id=generate_id(), - task_id=task.id, - order_id=order_id, - order_item_id=item_data.get("order_item_id"), - product_id=item_data.get("product_id"), - client_id=item_data.get("client_id"), - delivery_address=item_data.get("delivery_address"), - contact_person=item_data.get("contact_person"), - contact_phone=item_data.get("contact_phone"), - quantity=Decimal(str(item_data.get("quantity", 0))), - status="pending", - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(task_item) - task_items.append(task_item) - - db.commit() - db.refresh(task) - for item in task_items: - db.refresh(item) - - return task, task_items - - def get_expiring_tasks( - self, - db: Session, - within_minutes: int = 60, - region_id: str = None - ) -> List[BizDeliveryTask]: - """获取即将过期的配送任务(用于定时任务)""" - now = datetime.now() - expire_threshold = now + timedelta(minutes=within_minutes) - - query = db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.status.in_(["pending", "assigned", "picking"]), - BizDeliveryTask.is_del == 0, - BizDeliveryTask.effective_time <= expire_threshold, - BizDeliveryTask.effective_time >= now - ) - ) - - if region_id: - query = query.filter(BizDeliveryTask.region_id == region_id) - - return query.order_by(BizDeliveryTask.effective_time.asc()).all() - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizDeliveryTask]: - """更新配送任务并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": generate_id(), - "task_no": original.task_no, - "order_id": original.order_id, - "region_id": original.region_id, - "supplier_id": original.supplier_id, - "deliver_id": original.deliver_id, - "status": original.status, - "deliverer_status": getattr(original, "deliverer_status", None), - "supplier_prepare_status": getattr(original, "supplier_prepare_status", None), - "pickup_time": original.pickup_time, - "pickup_remark": original.pickup_remark, - "pickup_image_url": original.pickup_image_url, - "delivery_time": original.delivery_time, - "delivery_remark": original.delivery_remark, - "delivery_image_url": original.delivery_image_url, - "expire_warning_sent": getattr(original, "expire_warning_sent", False), - "effective_time": original.effective_time, - "dispatch_remark": getattr(original, "dispatch_remark", None), - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizDeliveryTaskHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - # ========================================= - # 抢单相关方法 - # ========================================= - - def get_pending_snatch_tasks( - self, - db: Session, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizDeliveryTask], int]: - """获取待抢单的任务(状态为pending且未分配配送员)""" - query = db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.status == "pending", - BizDeliveryTask.is_del == 0 - ) - ) - - total = query.count() - tasks = query.order_by(BizDeliveryTask.effective_time.asc()).offset(skip).limit(limit).all() - return tasks, total - - def count_pending_snatch_tasks(self, db: Session) -> int: - """统计待抢单的任务数量""" - return db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.status == "pending", - BizDeliveryTask.is_del == 0 - ) - ).count() - - def assign_to_deliver( - self, - db: Session, - task_id: str, - deliver_id: str, - deliver_name: str = None - ) -> bool: - """分配任务给配送员(用于抢单)""" - task = self.get(db, task_id) - if not task: - return False - - if task.status != "pending": - return False - - if task.deliver_id is not None: - return False - - task.deliver_id = deliver_id - task.deliver_name = deliver_name - task.deliverer_status = "pending_pickup" - task.supplier_prepare_status = "pending_prepare" - self._sync_legacy_status_from_dual(task) - task.update_at = datetime.now() - - db.commit() - db.refresh(task) - return True - - def get_snatched_by_me( - self, - db: Session, - deliver_id: str, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizDeliveryTask], int]: - """获取我抢到的任务""" - query = db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.deliver_id == deliver_id, - BizDeliveryTask.status.in_(["pending", "assigned"]), - BizDeliveryTask.is_del == 0 - ) - ) - - total = query.count() - tasks = query.order_by(BizDeliveryTask.effective_time.asc()).offset(skip).limit(limit).all() - return tasks, total - - def count_snatched_by_me(self, db: Session, deliver_id: str) -> int: - """统计我抢到的任务数量""" - return db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.deliver_id == deliver_id, - BizDeliveryTask.status.in_(["pending", "assigned"]), - BizDeliveryTask.is_del == 0 - ) - ).count() - - def count_today_snatched(self, db: Session, deliver_id: str, start_time: datetime) -> int: - """统计今日抢单成功数量""" - return db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.deliver_id == deliver_id, - BizDeliveryTask.status.in_(["pending", "assigned", "picking", "delivering", "completed"]), - BizDeliveryTask.is_del == 0, - BizDeliveryTask.update_at >= start_time - ) - ).count() - - def cancel_snatch(self, db: Session, task_id: str) -> bool: - """取消抢单""" - task = self.get(db, task_id) - if not task: - return False - - if task.status not in ["pending", "assigned"]: - return False - - task.deliver_id = None - task.deliver_name = None - task.status = "pending" - task.update_at = datetime.now() - - db.commit() - db.refresh(task) - return True - - def get_history_by_deliver( - self, - db: Session, - deliver_id: str, - start_date: str = None, - end_date: str = None, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizDeliveryTask], int]: - """获取配送员历史任务(已完成和已取消的)""" - query = db.query(BizDeliveryTask).filter( - and_( - BizDeliveryTask.deliver_id == deliver_id, - BizDeliveryTask.status.in_(["completed", "cancelled"]), - BizDeliveryTask.is_del == 0 - ) - ) - - if start_date: - query = query.filter(BizDeliveryTask.create_at >= start_date) - if end_date: - query = query.filter(BizDeliveryTask.create_at <= end_date + " 23:59:59") - - total = query.count() - tasks = query.order_by(BizDeliveryTask.delivery_time.desc()).offset(skip).limit(limit).all() - return tasks, total - - -class DeliveryTaskItemCRUD(CRUDBase[BizDeliveryTaskItem]): - """配送任务明细CRUD操作""" - - def get_by_task(self, db: Session, task_id: str) -> List[BizDeliveryTaskItem]: - """获取配送任务的所有明细""" - return db.query(BizDeliveryTaskItem).filter( - and_( - BizDeliveryTaskItem.task_id == task_id, - BizDeliveryTaskItem.is_del == 0 - ) - ).all() - - def get_delivered_quantity_by_order_item( - self, db: Session, order_id: str - ) -> dict[str, float]: - """按订单行汇总当前配送任务明细上的已送达数量(与 biz_order_item.delivered_quantity 对齐展示用)""" - task = db.query(BizDeliveryTask).filter( - and_(BizDeliveryTask.order_id == order_id, BizDeliveryTask.is_del == 0) - ).first() - if not task: - return {} - out: dict[str, float] = {} - for ti in self.get_by_task(db, task.id): - oid = ti.order_item_id - if not oid: - continue - q = float(ti.delivered_quantity or 0) - prev = out.get(oid, 0.0) - if q > prev: - out[oid] = q - return out - - def create_items( - self, - db: Session, - task_id: str, - items: List[dict], - creator: dict = None - ) -> List[BizDeliveryTaskItem]: - """批量创建配送任务明细""" - result = [] - for item in items: - task_item = BizDeliveryTaskItem( - id=generate_id(), - task_id=task_id, - order_id=item.get("order_id"), - order_item_id=item.get("order_item_id"), - product_id=item.get("product_id"), - client_id=item.get("client_id"), - delivery_address=item.get("delivery_address"), - contact_person=item.get("contact_person"), - contact_phone=item.get("contact_phone"), - quantity=Decimal(str(item.get("quantity", 0))), - status="pending", - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(task_item) - result.append(task_item) - db.commit() - for item in result: - db.refresh(item) - return result - - def update_item_status( - self, - db: Session, - item_id: str, - delivered_quantity: Decimal, - status: str = "delivered" - ) -> Optional[BizDeliveryTaskItem]: - """更新配送明细状态""" - item = self.get(db, item_id) - if not item: - return None - - item.delivered_quantity = delivered_quantity - item.status = status - if status == "delivered": - item.delivery_time = datetime.now() - - if item.order_item_id: - from app.crud.order import order_item_crud - - oi = order_item_crud.get(db, item.order_item_id) - if oi is not None: - oi.delivered_quantity = delivered_quantity - oi.update_at = datetime.now() - - db.commit() - db.refresh(item) - return item - - -delivery_task_crud = DeliveryTaskCRUD(BizDeliveryTask) -delivery_task_item_crud = DeliveryTaskItemCRUD(BizDeliveryTaskItem) diff --git a/backend/app/crud/delivery_warning_resolve.py b/backend/app/crud/delivery_warning_resolve.py deleted file mode 100644 index dc2e5fd9..00000000 --- a/backend/app/crud/delivery_warning_resolve.py +++ /dev/null @@ -1,55 +0,0 @@ -"""配送预警:任务完成/取消时自动结案(写历史后更新主表)""" -from datetime import datetime - -from sqlalchemy.orm import Session - -from app.models.models import BizDeliveryWarning, BizDeliveryWarningHis - - -def auto_resolve_open_warnings_for_task(db: Session, task_id: str) -> int: - """将某配送任务下未结案预警全部自动处理(配送完成等场景)。""" - warnings = ( - db.query(BizDeliveryWarning) - .filter( - BizDeliveryWarning.task_id == task_id, - BizDeliveryWarning.is_del == 0, - BizDeliveryWarning.is_resolved == 0, - ) - .all() - ) - if not warnings: - return 0 - - now = datetime.now() - for warning in warnings: - his = BizDeliveryWarningHis( - id=warning.id, - task_id=warning.task_id, - order_id=warning.order_id, - warning_type=warning.warning_type, - urgency_level=warning.urgency_level, - remaining_minutes=warning.remaining_minutes, - message=warning.message, - is_sent=warning.is_sent, - sent_time=warning.sent_time, - is_resolved=warning.is_resolved, - resolved_time=warning.resolved_time, - resolved_by=warning.resolved_by, - resolved_note=warning.resolved_note, - create_at=warning.create_at, - create_by=warning.create_by, - create_name=warning.create_name, - update_at=warning.update_at, - update_by=warning.update_by, - his_create_at=now, - ) - db.add(his) - warning.is_resolved = 1 - warning.resolved_time = now - warning.resolved_by = "system" - warning.resolved_note = "auto:配送已完成" - warning.update_at = now - warning.update_by = "system" - - db.commit() - return len(warnings) diff --git a/backend/app/crud/inquiry.py b/backend/app/crud/inquiry.py deleted file mode 100644 index bae71e37..00000000 --- a/backend/app/crud/inquiry.py +++ /dev/null @@ -1,231 +0,0 @@ -"""询价单CRUD""" -from typing import Optional, List -from datetime import datetime -from sqlalchemy.orm import Session -from sqlalchemy import and_, func - -from app.models.models import BizInquiry, BizInquiryItem, BizInquiryItemHis, BizInquiryHis -from app.crud.base import CRUDBase -from app.core.utils import generate_id - - -class InquiryCRUD(CRUDBase[BizInquiry]): - """询价单CRUD操作""" - - def get_by_region( - self, - db: Session, - region_id: str, - status: str = None, - skip: int = 0, - limit: int = 20 - ) -> List[BizInquiry]: - """根据区域获取询价单列表""" - query = db.query(BizInquiry).filter( - and_( - BizInquiry.region_id == region_id, - BizInquiry.is_del == 0 - ) - ) - if status: - query = query.filter(BizInquiry.status == status) - return query.order_by(BizInquiry.create_at.desc()).offset(skip).limit(limit).all() - - def get_published_by_region( - self, - db: Session, - region_id: str, - skip: int = 0, - limit: int = 20 - ) -> List[BizInquiry]: - """获取区域已发布的询价单列表""" - return self.get_by_region(db, region_id, status="published", skip=skip, limit=limit) - - def get_multi_with_filters( - self, - db: Session, - region_id: str = None, - status: str = None, - keyword: str = None, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizInquiry], int]: - """多条件查询询价单""" - query = db.query(BizInquiry).filter(BizInquiry.is_del == 0) - - if region_id: - query = query.filter(BizInquiry.region_id == region_id) - if status: - query = query.filter(BizInquiry.status == status) - if keyword: - query = query.filter(BizInquiry.title.like(f"%{keyword}%")) - - total = query.count() - inquiries = query.order_by(BizInquiry.create_at.desc()).offset(skip).limit(limit).all() - return inquiries, total - - def publish(self, db: Session, id: str) -> Optional[BizInquiry]: - """发布询价单""" - inquiry = self.get(db, id) - if inquiry and inquiry.status == "draft": - inquiry.status = "published" - inquiry.publish_time = datetime.now() - db.commit() - db.refresh(inquiry) - return inquiry - - def unpublish(self, db: Session, id: str) -> Optional[BizInquiry]: - """取消发布询价单(published -> draft)""" - inquiry = self.get(db, id) - if inquiry and inquiry.status == "published": - inquiry.status = "draft" - # 保留 publish_time 作为历史痕迹;如需清空可在这里置 None - db.commit() - db.refresh(inquiry) - return inquiry - - def get_current_published_by_region(self, db: Session, region_id: str) -> Optional[BizInquiry]: - """获取区域当前发布中的询价单(最多1条,业务上应保证互斥)""" - return db.query(BizInquiry).filter( - and_( - BizInquiry.region_id == region_id, - BizInquiry.status == "published", - BizInquiry.is_del == 0 - ) - ).order_by(BizInquiry.publish_time.desc(), BizInquiry.create_at.desc()).first() - - def close(self, db: Session, id: str) -> Optional[BizInquiry]: - """关闭询价单""" - inquiry = self.get(db, id) - if inquiry and inquiry.status == "published": - inquiry.status = "closed" - inquiry.close_time = datetime.now() - db.commit() - db.refresh(inquiry) - return inquiry - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizInquiry]: - """更新询价单并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "region_id": original.region_id, - "title": original.title, - "status": original.status, - "publish_time": original.publish_time, - "close_time": original.close_time, - "remark": original.remark, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizInquiryHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, id: str) -> bool: - """删除询价单并记录历史""" - original = self.get(db, id) - if not original: - return False - - his_data = { - "id": original.id, - "region_id": original.region_id, - "title": original.title, - "status": original.status, - "publish_time": original.publish_time, - "close_time": original.close_time, - "remark": original.remark, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizInquiryHis(**his_data) - db.add(his) - - return self.delete(db, id) - - -class InquiryItemCRUD(CRUDBase[BizInquiryItem]): - """询价单项CRUD操作""" - - def get_by_inquiry(self, db: Session, inquiry_id: str) -> List[BizInquiryItem]: - """获取询价单的所有询价项""" - return db.query(BizInquiryItem).filter( - and_( - BizInquiryItem.inquiry_id == inquiry_id, - BizInquiryItem.is_del == 0 - ) - ).all() - - def get_multi_with_inquiry_ids( - self, - db: Session, - inquiry_ids: List[str] - ) -> List[BizInquiryItem]: - """根据多个询价单ID获取询价项""" - return db.query(BizInquiryItem).filter( - and_( - BizInquiryItem.inquiry_id.in_(inquiry_ids), - BizInquiryItem.is_del == 0 - ) - ).all() - - def create_items( - self, - db: Session, - inquiry_id: str, - product_ids: List[str], - creator: dict = None - ) -> List[BizInquiryItem]: - """批量创建询价项""" - items = [] - for product_id in product_ids: - item = BizInquiryItem( - id=generate_id(), - inquiry_id=inquiry_id, - product_id=product_id, - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(item) - items.append(item) - db.commit() - for item in items: - db.refresh(item) - return items - - def update_items( - self, - db: Session, - inquiry_id: str, - product_ids: List[str], - creator: dict = None - ) -> List[BizInquiryItem]: - """更新询价项(先删除旧的,再创建新的)""" - existing_items = self.get_by_inquiry(db, inquiry_id) - for item in existing_items: - item.is_del = 1 - - return self.create_items(db, inquiry_id, product_ids, creator) - - def delete_by_inquiry(self, db: Session, inquiry_id: str) -> bool: - """删除询价单的所有询价项""" - items = self.get_by_inquiry(db, inquiry_id) - for item in items: - item.is_del = 1 - db.commit() - return True - - -inquiry_crud = InquiryCRUD(BizInquiry) -inquiry_item_crud = InquiryItemCRUD(BizInquiryItem) diff --git a/backend/app/crud/ord_settlement.py b/backend/app/crud/ord_settlement.py deleted file mode 100644 index dd918594..00000000 --- a/backend/app/crud/ord_settlement.py +++ /dev/null @@ -1,314 +0,0 @@ -"""客户/供应商结算单 CRUD(含 UPDATE 写历史表)""" -from __future__ import annotations - -from datetime import datetime -from typing import Any, Dict, List, Optional, Tuple - -from sqlalchemy.orm import Session - -from app.crud.base import CRUDBase -from app.models.models import ( - BizOrder, - OrdClientSettlement, - OrdClientSettlementHis, - OrdClientSettlementItem, - OrdClientSettlementItemHis, - OrdSupplierSettlement, - OrdSupplierSettlementHis, - OrdSupplierSettlementItem, - OrdSupplierSettlementItemHis, -) - - -class OrdClientSettlementCRUD(CRUDBase[OrdClientSettlement]): - def _snapshot_main(self, row: OrdClientSettlement) -> Dict[str, Any]: - return { - "id": row.id, - "settlement_no": row.settlement_no, - "client_id": row.client_id, - "client_name": row.client_name, - "order_count": row.order_count, - "total_amount": row.total_amount, - "voucher_urls": row.voucher_urls, - "remark": row.remark, - "settlement_at": row.settlement_at, - "status": row.status, - "create_at": row.create_at, - "create_by": row.create_by, - "create_name": row.create_name, - "update_at": row.update_at, - "update_by": row.update_by, - } - - def get_multi_with_filters( - self, - db: Session, - *, - client_id: Optional[str] = None, - settlement_no: Optional[str] = None, - start_date: Optional[datetime] = None, - end_date: Optional[datetime] = None, - region_id: Optional[str] = None, - skip: int = 0, - limit: int = 10, - ) -> Tuple[List[OrdClientSettlement], int]: - q = db.query(OrdClientSettlement).filter(OrdClientSettlement.is_del == 0) - if client_id: - q = q.filter(OrdClientSettlement.client_id == client_id) - if settlement_no: - q = q.filter(OrdClientSettlement.settlement_no.like(f"%{settlement_no}%")) - if start_date: - q = q.filter(OrdClientSettlement.settlement_at >= start_date) - if end_date: - q = q.filter(OrdClientSettlement.settlement_at <= end_date) - if region_id: - sub = ( - db.query(OrdClientSettlementItem.settlement_id) - .join(BizOrder, BizOrder.id == OrdClientSettlementItem.order_id) - .filter( - OrdClientSettlementItem.is_del == 0, - BizOrder.is_del == 0, - BizOrder.region_id == region_id, - ) - .distinct() - ) - q = q.filter(OrdClientSettlement.id.in_(sub)) - total = q.count() - rows = q.order_by(OrdClientSettlement.settlement_at.desc()).offset(skip).limit(limit).all() - return rows, total - - def get_with_items( - self, db: Session, settlement_id: str - ) -> Tuple[Optional[OrdClientSettlement], List[OrdClientSettlementItem]]: - main = self.get(db, settlement_id) - if not main or main.is_del == 1: - return None, [] - items = ( - db.query(OrdClientSettlementItem) - .filter( - OrdClientSettlementItem.settlement_id == settlement_id, - OrdClientSettlementItem.is_del == 0, - ) - .order_by(OrdClientSettlementItem.create_at.asc()) - .all() - ) - return main, items - - def belongs_to_region(self, db: Session, settlement_id: str, region_id: str) -> bool: - if not region_id: - return True - return ( - db.query(OrdClientSettlementItem.id) - .join(BizOrder, BizOrder.id == OrdClientSettlementItem.order_id) - .filter( - OrdClientSettlementItem.settlement_id == settlement_id, - OrdClientSettlementItem.is_del == 0, - BizOrder.is_del == 0, - BizOrder.region_id == region_id, - ) - .first() - is not None - ) - - def update_with_history( - self, - db: Session, - settlement_id: str, - obj_in: dict, - updater: Optional[dict] = None, - ) -> Optional[OrdClientSettlement]: - original = self.get(db, settlement_id) - if not original or original.is_del == 1: - return None - his = OrdClientSettlementHis(**self._snapshot_main(original)) - db.add(his) - return self.update(db, settlement_id, obj_in, updater) - - -class OrdClientSettlementItemCRUD(CRUDBase[OrdClientSettlementItem]): - def _snapshot_item(self, row: OrdClientSettlementItem) -> Dict[str, Any]: - return { - "id": row.id, - "settlement_id": row.settlement_id, - "order_id": row.order_id, - "order_no": row.order_no, - "client_id": row.client_id, - "order_amount": row.order_amount, - "create_at": row.create_at, - "create_by": row.create_by, - "create_name": row.create_name, - "update_at": row.update_at, - "update_by": row.update_by, - } - - def update_with_history( - self, - db: Session, - item_id: str, - obj_in: dict, - updater: Optional[dict] = None, - ) -> Optional[OrdClientSettlementItem]: - original = self.get(db, item_id) - if not original or original.is_del == 1: - return None - his = OrdClientSettlementItemHis(**self._snapshot_item(original)) - db.add(his) - return self.update(db, item_id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, item_id: str, updater: Optional[dict] = None) -> bool: - original = self.get(db, item_id) - if not original or original.is_del == 1: - return False - his = OrdClientSettlementItemHis(**self._snapshot_item(original)) - db.add(his) - return self.update(db, item_id, {"is_del": 1}, updater) is not None - - -class OrdSupplierSettlementCRUD(CRUDBase[OrdSupplierSettlement]): - def _snapshot_main(self, row: OrdSupplierSettlement) -> Dict[str, Any]: - return { - "id": row.id, - "settlement_no": row.settlement_no, - "supplier_id": row.supplier_id, - "supplier_name": row.supplier_name, - "order_count": row.order_count, - "total_amount": row.total_amount, - "voucher_urls": row.voucher_urls, - "remark": row.remark, - "settlement_at": row.settlement_at, - "status": row.status, - "create_at": row.create_at, - "create_by": row.create_by, - "create_name": row.create_name, - "update_at": row.update_at, - "update_by": row.update_by, - } - - def get_multi_with_filters( - self, - db: Session, - *, - supplier_id: Optional[str] = None, - settlement_no: Optional[str] = None, - start_date: Optional[datetime] = None, - end_date: Optional[datetime] = None, - region_id: Optional[str] = None, - skip: int = 0, - limit: int = 10, - ) -> Tuple[List[OrdSupplierSettlement], int]: - q = db.query(OrdSupplierSettlement).filter(OrdSupplierSettlement.is_del == 0) - if supplier_id: - q = q.filter(OrdSupplierSettlement.supplier_id == supplier_id) - if settlement_no: - q = q.filter(OrdSupplierSettlement.settlement_no.like(f"%{settlement_no}%")) - if start_date: - q = q.filter(OrdSupplierSettlement.settlement_at >= start_date) - if end_date: - q = q.filter(OrdSupplierSettlement.settlement_at <= end_date) - if region_id: - sub = ( - db.query(OrdSupplierSettlementItem.settlement_id) - .join(BizOrder, BizOrder.id == OrdSupplierSettlementItem.order_id) - .filter( - OrdSupplierSettlementItem.is_del == 0, - BizOrder.is_del == 0, - BizOrder.region_id == region_id, - ) - .distinct() - ) - q = q.filter(OrdSupplierSettlement.id.in_(sub)) - total = q.count() - rows = q.order_by(OrdSupplierSettlement.settlement_at.desc()).offset(skip).limit(limit).all() - return rows, total - - def get_with_items( - self, db: Session, settlement_id: str - ) -> Tuple[Optional[OrdSupplierSettlement], List[OrdSupplierSettlementItem]]: - main = self.get(db, settlement_id) - if not main or main.is_del == 1: - return None, [] - items = ( - db.query(OrdSupplierSettlementItem) - .filter( - OrdSupplierSettlementItem.settlement_id == settlement_id, - OrdSupplierSettlementItem.is_del == 0, - ) - .order_by(OrdSupplierSettlementItem.create_at.asc()) - .all() - ) - return main, items - - def belongs_to_region(self, db: Session, settlement_id: str, region_id: str) -> bool: - if not region_id: - return True - return ( - db.query(OrdSupplierSettlementItem.id) - .join(BizOrder, BizOrder.id == OrdSupplierSettlementItem.order_id) - .filter( - OrdSupplierSettlementItem.settlement_id == settlement_id, - OrdSupplierSettlementItem.is_del == 0, - BizOrder.is_del == 0, - BizOrder.region_id == region_id, - ) - .first() - is not None - ) - - def update_with_history( - self, - db: Session, - settlement_id: str, - obj_in: dict, - updater: Optional[dict] = None, - ) -> Optional[OrdSupplierSettlement]: - original = self.get(db, settlement_id) - if not original or original.is_del == 1: - return None - his = OrdSupplierSettlementHis(**self._snapshot_main(original)) - db.add(his) - return self.update(db, settlement_id, obj_in, updater) - - -class OrdSupplierSettlementItemCRUD(CRUDBase[OrdSupplierSettlementItem]): - def _snapshot_item(self, row: OrdSupplierSettlementItem) -> Dict[str, Any]: - return { - "id": row.id, - "settlement_id": row.settlement_id, - "order_id": row.order_id, - "order_no": row.order_no, - "supplier_id": row.supplier_id, - "order_amount": row.order_amount, - "create_at": row.create_at, - "create_by": row.create_by, - "create_name": row.create_name, - "update_at": row.update_at, - "update_by": row.update_by, - } - - def update_with_history( - self, - db: Session, - item_id: str, - obj_in: dict, - updater: Optional[dict] = None, - ) -> Optional[OrdSupplierSettlementItem]: - original = self.get(db, item_id) - if not original or original.is_del == 1: - return None - his = OrdSupplierSettlementItemHis(**self._snapshot_item(original)) - db.add(his) - return self.update(db, item_id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, item_id: str, updater: Optional[dict] = None) -> bool: - original = self.get(db, item_id) - if not original or original.is_del == 1: - return False - his = OrdSupplierSettlementItemHis(**self._snapshot_item(original)) - db.add(his) - return self.update(db, item_id, {"is_del": 1}, updater) is not None - - -ord_client_settlement_crud = OrdClientSettlementCRUD(OrdClientSettlement) -ord_client_settlement_item_crud = OrdClientSettlementItemCRUD(OrdClientSettlementItem) -ord_supplier_settlement_crud = OrdSupplierSettlementCRUD(OrdSupplierSettlement) -ord_supplier_settlement_item_crud = OrdSupplierSettlementItemCRUD(OrdSupplierSettlementItem) diff --git a/backend/app/crud/order.py b/backend/app/crud/order.py deleted file mode 100644 index 0caf7741..00000000 --- a/backend/app/crud/order.py +++ /dev/null @@ -1,1006 +0,0 @@ -"""订单CRUD""" -from typing import Optional, List -import json -from datetime import datetime -from decimal import Decimal -from sqlalchemy.orm import Session -from sqlalchemy import and_, or_, func, select, exists - -from app.models.models import BizOrder, BizOrderItem, BizOrderStatusLog, BizOrderHis, BizOrderAdjust, SysUser -from app.crud.base import CRUDBase -from app.core.utils import generate_id -from app.core.order_status_compat import derive_three_status_from_legacy, derive_legacy_status_from_three - - -class OrderCRUD(CRUDBase[BizOrder]): - @staticmethod - def normalize_payment_method(payment_method: Optional[str]) -> str: - """统一支付方式值,仅保留 online/offline。""" - method = (payment_method or "").strip().lower() - if method in ("wechat", "wx", "online"): - return "online" - if method == "offline": - return "offline" - return "online" - - def sync_pay_fields_from_amounts(self, order: BizOrder) -> None: - """根据已付/应收同步 pay_status、payment_time 与兼容字段 status(付清后才是 paid)。""" - pa = order.paid_amount or Decimal("0") - ra = order.receivable_amount or Decimal("0") - if ra > 0 and pa >= ra: - order.pay_status = "paid" - if not order.payment_time: - order.payment_time = datetime.now() - else: - order.pay_status = "pending_payment" - order.status = derive_legacy_status_from_three( - order_status=getattr(order, "order_status", None) or "pending_confirm", - delivery_status=getattr(order, "delivery_status", None) or "pending_dispatch", - pay_status=order.pay_status, - ) - - """订单CRUD操作""" - - def _sync_three_status_fields_from_legacy(self, order: BizOrder): - """阶段A:保持三字段与旧 status 一致(先兼容,再逐步迁移逻辑)。""" - ts = derive_three_status_from_legacy(order.status, paid_amount=order.paid_amount) - order.order_status = ts.order_status - order.delivery_status = ts.delivery_status - order.pay_status = ts.pay_status - - def _apply_three_status(self, order: BizOrder, *, order_status: str = None, delivery_status: str = None, pay_status: str = None): - """阶段D收尾:优先写三字段,再回写兼容 status。""" - if order_status is not None: - order.order_status = order_status - if delivery_status is not None: - order.delivery_status = delivery_status - if pay_status is not None: - order.pay_status = pay_status - order.status = derive_legacy_status_from_three( - order_status=order.order_status, - delivery_status=order.delivery_status, - pay_status=order.pay_status, - ) - - def get_by_client(self, db: Session, client_id: str) -> List[BizOrder]: - """根据客户获取订单列表""" - return db.query(BizOrder).filter( - and_( - BizOrder.client_id == client_id, - BizOrder.is_del == 0 - ) - ).order_by(BizOrder.create_at.desc()).all() - - def get_multi_with_filters( - self, - db: Session, - client_id: str = None, - region_id: str = None, - status: str = None, - order_status: str = None, - delivery_status: str = None, - pay_status: str = None, - payment_method: str = None, - cancel_status: str = None, - order_no: str = None, - keyword: str = None, - start_date: str = None, - end_date: str = None, - skip: int = 0, - limit: int = 10, - list_tab: str = None, - ) -> tuple[List[BizOrder], int]: - """多条件查询订单""" - query = db.query(BizOrder).filter(BizOrder.is_del == 0) - - if client_id: - query = query.filter(BizOrder.client_id == client_id) - if region_id: - query = query.filter(BizOrder.region_id == region_id) - if status: - query = query.filter(BizOrder.status == status) - if order_status: - query = query.filter(BizOrder.order_status == order_status) - if delivery_status: - query = query.filter(BizOrder.delivery_status == delivery_status) - if pay_status: - query = query.filter(BizOrder.pay_status == pay_status) - if payment_method: - pm = self.normalize_payment_method(payment_method) - if pm == "offline": - query = query.filter(BizOrder.payment_method == "offline") - else: - query = query.filter( - or_( - BizOrder.payment_method == "online", - BizOrder.payment_method.is_(None), - BizOrder.payment_method == "", - ) - ) - if cancel_status: - query = query.filter(BizOrder.cancel_status == cancel_status) - if order_no: - query = query.filter(BizOrder.order_no.like(f"%{order_no}%")) - if keyword: - kw = (keyword or "").strip() - if kw: - pattern = f"%{kw}%" - client_user_match = exists( - select(1).select_from(SysUser).where( - and_( - SysUser.id == BizOrder.client_id, - SysUser.is_del == 0, - or_( - SysUser.real_name.like(pattern), - SysUser.username.like(pattern), - SysUser.phone.like(pattern), - ), - ) - ) - ) - query = query.filter( - or_( - BizOrder.order_no.like(pattern), - BizOrder.contact_person.like(pattern), - BizOrder.contact_phone.like(pattern), - client_user_match, - ) - ) - if start_date: - query = query.filter(BizOrder.create_at >= start_date) - if end_date: - query = query.filter(BizOrder.create_at <= end_date) - - # 客户端订单列表 Tab(与小程序 order.vue 分段一致,保证分页有效) - if list_tab == "pending_payment": - query = query.filter( - and_( - BizOrder.order_status == "pending_payment", - BizOrder.pay_status == "pending_payment", - BizOrder.cancel_status != "pending_review", - ) - ) - elif list_tab == "pending_confirm": - query = query.filter( - and_( - BizOrder.order_status == "pending_confirm", - BizOrder.cancel_status != "pending_review", - ) - ) - elif list_tab == "completed": - query = query.filter(BizOrder.order_status == "completed") - elif list_tab == "cancelled": - query = query.filter(BizOrder.order_status == "cancelled") - - total = query.count() - orders = query.order_by(BizOrder.create_at.desc()).offset(skip).limit(limit).all() - return orders, total - - def get_by_order_no(self, db: Session, order_no: str) -> Optional[BizOrder]: - """根据订单号获取订单""" - return db.query(BizOrder).filter( - and_( - BizOrder.order_no == order_no, - BizOrder.is_del == 0 - ) - ).first() - - def create_order( - self, - db: Session, - client_id: str, - region_id: str, - price_list_id: str, - price_version: int, - items: List[dict], - address_id: str, - address_info: dict, - payment_method: str, - remark: str = None, - creator: dict = None - ) -> BizOrder: - """创建订单""" - # 生成订单号 - order_no = self._generate_order_no(db) - - # 计算订单总金额 - total_amount = sum( - Decimal(str(item.get("quantity", 0))) * Decimal(str(item.get("unitPrice", 0))) - for item in items - ) - - normalized_payment_method = self.normalize_payment_method(payment_method) - - # 线上:未支付前主状态为待支付;线下:未收款前 pay_status 仍为待支付,但主状态进入待收货/履约阶段,可与 pending_dispatch 一并参与分配配送 - if normalized_payment_method == "offline": - init_order_status = "pending_confirm" - else: - init_order_status = "pending_payment" - init_delivery_status = "pending_dispatch" - init_pay_status = "pending_payment" - order = BizOrder( - id=generate_id(), - order_no=order_no, - region_id=region_id, - client_id=client_id, - price_list_id=price_list_id, - price_version=price_version, - status=derive_legacy_status_from_three( - order_status=init_order_status, - delivery_status=init_delivery_status, - pay_status=init_pay_status, - ), - order_status=init_order_status, - delivery_status=init_delivery_status, - pay_status=init_pay_status, - cancel_status="none", - payment_method=normalized_payment_method, - client_settlement_status="settled" if normalized_payment_method == "online" else "pending", - supplier_settlement_status="pending", - client_settlement_at=datetime.now() if normalized_payment_method == "online" else None, - supplier_settlement_at=None, - total_amount=total_amount, - paid_amount=Decimal("0"), - receivable_amount=total_amount, - refund_amount=Decimal("0"), - contact_person=address_info.get("contactPerson"), - contact_phone=address_info.get("contactPhone"), - delivery_address=address_info.get("fullAddress"), - remark=remark, - payment_time=None, - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(order) - db.flush() - - # 创建订单项 - for item in items: - amount = Decimal(str(item.get("quantity", 0))) * Decimal(str(item.get("unitPrice", 0))) - order_item = BizOrderItem( - id=generate_id(), - order_id=order.id, - product_id=item.get("productId"), - unit_id=item.get("unitId"), - quantity=Decimal(str(item.get("quantity", 0))), - unit_price=Decimal(str(item.get("unitPrice", 0))), - amount=amount, - delivered_quantity=Decimal("0"), - status="normal", - note=item.get("note"), - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(order_item) - - # 创建订单状态日志 - self._create_status_log( - db, - order.id, - None, - order.status, - creator.get("user_id") if creator else client_id, - "client", - "创建订单" - ) - - db.commit() - db.refresh(order) - return order - - def update_status( - self, - db: Session, - order_id: str, - new_status: str, - operator_id: str, - operator_role: str, - reason: str = None, - note: str = None - ) -> Optional[BizOrder]: - """更新订单状态""" - order = self.get(db, order_id) - if not order: - return None - - old_status = order.status - order.status = new_status - order.update_at = datetime.now() - - # 特殊状态处理 - if new_status == "paid": - order.payment_time = datetime.now() - elif new_status == "completed": - order.completed_time = datetime.now() - order.completed_by = operator_id - - # 阶段A:同步三字段 - self._sync_three_status_fields_from_legacy(order) - - # 创建状态变更日志 - self._create_status_log( - db, - order_id, - old_status, - new_status, - operator_id, - operator_role, - reason, - note - ) - - db.commit() - db.refresh(order) - return order - - def cancel_order( - self, - db: Session, - order_id: str, - cancel_type: str, - reason: str, - operator_id: str, - operator_role: str - ) -> Optional[BizOrder]: - """取消订单""" - order = self.get(db, order_id) - if not order: - return None - - # 待支付订单直接取消 - if order.status == "pending_payment": - return self.update_status( - db, - order_id, - "cancelled", - operator_id, - operator_role, - reason=reason - ) - - # 已支付订单申请取消 - if order.delivery_status in ["pending_dispatch", "delivering"]: - self._apply_three_status(order, order_status="pending_confirm") - order.cancel_status = "pending_review" - order.cancel_reason = reason - order.update_at = datetime.now() - - self._create_status_log( - db, - order_id, - order.status, - "pending_review_cancel", # 兼容历史日志标识 - operator_id, - operator_role, - reason=reason - ) - - db.commit() - db.refresh(order) - return order - - return None - - def review_cancel( - self, - db: Session, - order_id: str, - approved: bool, - reviewer_id: str, - review_note: str = None - ) -> Optional[BizOrder]: - """审核取消订单""" - order = self.get(db, order_id) - if not order or order.cancel_status != "pending_review": - return None - - if approved: - order.cancel_status = "approved" - self._apply_three_status(order, order_status="cancelled") - new_status = order.status - else: - # 从状态日志获取取消前的状态 - from app.models.models import BizOrderStatusLog - cancel_log = db.query(BizOrderStatusLog).filter( - BizOrderStatusLog.order_id == order_id, - BizOrderStatusLog.to_status == "pending_review_cancel" - ).order_by(BizOrderStatusLog.create_at.desc()).first() - - # 恢复为取消前的状态(pending_delivery, delivering, partial_delivered) - if cancel_log and cancel_log.from_status: - # 兼容历史:尽量从旧状态恢复到三字段 - order.status = cancel_log.from_status - self._sync_three_status_fields_from_legacy(order) - else: - self._apply_three_status(order, order_status="pending_confirm", delivery_status="pending_dispatch") - order.cancel_status = "rejected" - new_status = order.status - - order.cancel_review_time = datetime.now() - order.cancel_reviewed_by = reviewer_id - order.cancel_review_note = review_note - order.update_at = datetime.now() - - self._create_status_log( - db, - order_id, - "pending_review_cancel", - new_status, - reviewer_id, - "admin", - reason=f"审核{'通过' if approved else '拒绝'}: {review_note}" - ) - - db.commit() - db.refresh(order) - return order - - def confirm_receive( - self, - db: Session, - order_id: str, - operator_id: str - ) -> Optional[BizOrder]: - """确认收货""" - order = self.get(db, order_id) - if not order: - return None - - # 仅待确认收货状态支持确认收货 - if order.status == "pending_confirm": - return self.update_status( - db, - order_id, - "completed", - operator_id, - "client", - reason="客户确认收货" - ) - return None - - def auto_confirm_receive_overdue( - self, - db: Session, - *, - before_threshold: datetime, - configured_days: int, - ) -> int: - """送达后超过 configured_days 未确认收货的订单,系统自动确认(与 force_complete 一致写三字段与日志)。""" - threshold_expr = func.coalesce(BizOrder.delivered_time, BizOrder.update_at) - orders = ( - db.query(BizOrder) - .filter( - BizOrder.is_del == 0, - BizOrder.order_status == "pending_confirm", - BizOrder.delivery_status == "delivered", - BizOrder.cancel_status.in_(("none", "rejected")), - threshold_expr <= before_threshold, - ) - .all() - ) - if not orders: - return 0 - note = f"送达后超过{configured_days}天未确认收货,系统自动确认" - count = 0 - for order in orders: - old_status = order.status - self._apply_three_status(order, order_status="completed") - order.completed_time = datetime.now() - order.completed_by = "system" - order.update_at = datetime.now() - self._create_status_log( - db, - order.id, - old_status, - "completed", - "system", - "system", - reason=note, - ) - count += 1 - db.commit() - return count - - def _generate_order_no(self, db: Session) -> str: - """生成订单号""" - today = datetime.now().strftime("%Y%m%d") - prefix = f"ORD{today}" - - # 查询当天最大订单号 - last_order = db.query(BizOrder).filter( - BizOrder.order_no.like(f"{prefix}%") - ).order_by(BizOrder.order_no.desc()).first() - - if last_order: - last_no = int(last_order.order_no[-4:]) - new_no = last_no + 1 - else: - new_no = 1 - - return f"{prefix}{str(new_no).zfill(4)}" - - def _create_status_log( - self, - db: Session, - order_id: str, - from_status: str, - to_status: str, - operator_id: str, - operator_role: str, - reason: str = None, - note: str = None, - create_name: str = None, - log_create_at: Optional[datetime] = None, - ): - """创建订单状态变更日志""" - ts = log_create_at or datetime.now() - log = BizOrderStatusLog( - id=generate_id(), - order_id=order_id, - from_status=from_status, - to_status=to_status, - operator_id=operator_id, - operator_role=operator_role, - reason=reason, - note=note, - create_by=operator_id, - create_name=create_name or None, - create_at=ts, - update_at=ts, - ) - db.add(log) - - def close_unpaid_online_orders( - self, - db: Session, - *, - before_time: datetime, - timeout_note: str, - ) -> int: - """关闭超时未支付的线上订单。""" - candidates = db.query(BizOrder).filter( - BizOrder.is_del == 0, - BizOrder.payment_method == "online", - BizOrder.pay_status == "pending_payment", - BizOrder.order_status == "pending_payment", - BizOrder.create_at <= before_time, - ).all() - count = 0 - for order in candidates: - self._apply_three_status(order, order_status="cancelled", delivery_status="pending_dispatch", pay_status="pending_payment") - order.cancel_status = "approved" - order.cancel_reason = "线上支付超时自动关闭" - order.cancel_review_note = "系统自动关闭" - order.update_at = datetime.now() - self._create_status_log( - db, - order.id, - "pending_payment", - "cancelled", - "system", - "system", - reason="线上支付超时自动关闭", - note=timeout_note, - ) - count += 1 - if count: - db.commit() - return count - - def admin_cancel( - self, - db: Session, - *, - order_id: str, - operator_id: str, - operator_name: str, - reason: str, - note: Optional[str] = None, - evidence_image_urls: Optional[List[str]] = None, - ) -> Optional[BizOrder]: - """运营取消订单。""" - order = self.get(db, order_id) - if not order: - return None - old_status = order.status - self._apply_three_status(order, order_status="cancelled") - order.cancel_status = "approved" - order.cancel_reason = reason - if note: - order.cancel_review_note = note - order.cancel_review_time = datetime.now() - order.cancel_reviewed_by = operator_id - order.update_at = datetime.now() - self._create_status_log( - db, - order.id, - old_status, - "cancelled", - operator_id, - "admin", - reason=reason, - note=json.dumps({"note": note or "", "evidenceImageUrls": evidence_image_urls or []}, ensure_ascii=False), - ) - db.commit() - db.refresh(order) - return order - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizOrder]: - """更新订单并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "order_no": original.order_no, - "region_id": original.region_id, - "client_id": original.client_id, - "price_list_id": original.price_list_id, - "price_version": original.price_version, - "status": original.status, - "order_status": getattr(original, "order_status", None), - "delivery_status": getattr(original, "delivery_status", None), - "pay_status": getattr(original, "pay_status", None), - "cancel_status": getattr(original, "cancel_status", None), - "payment_method": original.payment_method, - "client_settlement_status": getattr(original, "client_settlement_status", "pending"), - "supplier_settlement_status": getattr(original, "supplier_settlement_status", "pending"), - "client_settlement_at": getattr(original, "client_settlement_at", None), - "supplier_settlement_at": getattr(original, "supplier_settlement_at", None), - "total_amount": original.total_amount, - "paid_amount": original.paid_amount, - "receivable_amount": original.receivable_amount, - "refund_amount": original.refund_amount, - "contact_person": original.contact_person, - "contact_phone": original.contact_phone, - "delivery_address": original.delivery_address, - "cancel_reason": original.cancel_reason, - "cancel_review_time": original.cancel_review_time, - "cancel_reviewed_by": original.cancel_reviewed_by, - "cancel_review_note": original.cancel_review_note, - "completed_time": original.completed_time, - "completed_by": original.completed_by, - "remark": original.remark, - "payment_time": original.payment_time, - "delivery_assign_time": original.delivery_assign_time, - "delivered_time": getattr(original, "delivered_time", None), - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizOrderHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, id: str) -> bool: - """删除订单并记录历史""" - original = self.get(db, id) - if not original: - return False - - his_data = { - "id": original.id, - "order_no": original.order_no, - "region_id": original.region_id, - "client_id": original.client_id, - "price_list_id": original.price_list_id, - "price_version": original.price_version, - "status": original.status, - "order_status": getattr(original, "order_status", None), - "delivery_status": getattr(original, "delivery_status", None), - "pay_status": getattr(original, "pay_status", None), - "cancel_status": getattr(original, "cancel_status", None), - "payment_method": original.payment_method, - "client_settlement_status": getattr(original, "client_settlement_status", "pending"), - "supplier_settlement_status": getattr(original, "supplier_settlement_status", "pending"), - "client_settlement_at": getattr(original, "client_settlement_at", None), - "supplier_settlement_at": getattr(original, "supplier_settlement_at", None), - "total_amount": original.total_amount, - "paid_amount": original.paid_amount, - "receivable_amount": original.receivable_amount, - "refund_amount": original.refund_amount, - "contact_person": original.contact_person, - "contact_phone": original.contact_phone, - "delivery_address": original.delivery_address, - "delivered_time": getattr(original, "delivered_time", None), - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizOrderHis(**his_data) - db.add(his) - - return self.delete(db, id) - - def lock_order( - self, - db: Session, - order_id: str, - operator_id: str - ) -> Optional[BizOrder]: - """锁定订单,防止并发操作""" - order = self.get(db, order_id) - if not order: - return None - - # 如果已被其他操作员锁定,不能重复锁定 - if order.lock_operator_id and order.lock_operator_id != operator_id: - lock_time = order.lock_time.strftime("%Y-%m-%d %H:%M:%S") if order.lock_time else "未知" - raise ValueError(f"订单已被用户 {order.lock_operator_id} 于 {lock_time} 锁定") - - order.lock_operator_id = operator_id - order.lock_time = datetime.now() - order.update_at = datetime.now() - - db.commit() - db.refresh(order) - return order - - def unlock_order( - self, - db: Session, - order_id: str, - operator_id: str - ) -> Optional[BizOrder]: - """解锁订单""" - order = self.get(db, order_id) - if not order: - return None - - # 只能解锁自己锁定的订单 - if order.lock_operator_id and order.lock_operator_id != operator_id: - raise ValueError("只能解锁自己锁定的订单") - - order.lock_operator_id = None - order.lock_time = None - order.update_at = datetime.now() - - db.commit() - db.refresh(order) - return order - - def force_complete( - self, - db: Session, - order_id: str, - operator_id: str, - operator_role: str, - reason: str = None - ) -> Optional[BizOrder]: - """运营强制完结订单""" - order = self.get(db, order_id) - if not order: - return None - - # 只有待确认收货状态可以强制完结 - if order.order_status not in ["pending_confirm"]: - raise ValueError("只有待确认收货状态的订单可以强制完结") - - old_status = order.status - self._apply_three_status(order, order_status="completed") - order.completed_time = datetime.now() - order.completed_by = operator_id - order.update_at = datetime.now() - - # 记录状态变更 - self._create_status_log( - db, - order_id, - old_status, - "completed", - operator_id, - operator_role, - reason=reason or "运营强制完结" - ) - - db.commit() - db.refresh(order) - return order - - def update_to_partial_delivered( - self, - db: Session, - order_id: str, - operator_id: str, - operator_role: str, - reason: str = None - ) -> Optional[BizOrder]: - """已废弃:旧的部分送达流程不再支持。""" - raise ValueError("部分送达流程已下线") - - def update_to_confirm( - self, - db: Session, - order_id: str, - operator_id: str, - operator_role: str, - reason: str = None - ) -> Optional[BizOrder]: - """已废弃:旧的部分送达确认流程不再支持。""" - raise ValueError("部分送达确认流程已下线") - - -class OrderItemCRUD(CRUDBase[BizOrderItem]): - """订单项CRUD操作""" - - def get_by_order(self, db: Session, order_id: str) -> List[BizOrderItem]: - """获取订单的所有订单项""" - return db.query(BizOrderItem).filter( - and_( - BizOrderItem.order_id == order_id, - BizOrderItem.is_del == 0 - ) - ).all() - - def get_count_by_order(self, db: Session, order_id: str) -> int: - """获取订单项数量""" - return db.query(BizOrderItem).filter( - and_( - BizOrderItem.order_id == order_id, - BizOrderItem.is_del == 0 - ) - ).count() - - def update_item_adjustment( - self, - db: Session, - item_id: str, - new_quantity: Decimal = None, - new_amount: Decimal = None, - new_status: str = None, - note: str = None, - updater_id: str = None, - updater_name: str = None - ) -> Optional[BizOrderItem]: - """更新订单项(调整后)""" - item = self.get(db, item_id) - if not item: - return None - - if new_quantity is not None: - item.quantity = new_quantity - if new_amount is not None: - item.amount = new_amount - if new_status is not None: - item.status = new_status - if note is not None: - item.note = note - - item.update_at = datetime.now() - item.update_by = updater_id - - db.commit() - db.refresh(item) - return item - - def mark_as_reduced( - self, - db: Session, - item_id: str, - new_quantity: Decimal, - new_amount: Decimal, - note: str = None - ) -> Optional[BizOrderItem]: - """标记为已减量""" - return self.update_item_adjustment( - db, item_id, - new_quantity=new_quantity, - new_amount=new_amount, - new_status="reduced", - note=note - ) - - def mark_as_deleted( - self, - db: Session, - item_id: str, - note: str = None - ) -> Optional[BizOrderItem]: - """标记为已删除""" - return self.update_item_adjustment( - db, item_id, - new_status="deleted", - note=note - ) - - def mark_as_substituted( - self, - db: Session, - item_id: str, - new_product_id: str, - new_quantity: Decimal, - new_amount: Decimal, - note: str = None - ) -> Optional[BizOrderItem]: - """标记为已替代""" - item = self.update_item_adjustment( - db, item_id, - new_status="substituted", - note=note - ) - return item - - -class OrderStatusLogCRUD(CRUDBase[BizOrderStatusLog]): - """订单状态日志CRUD操作""" - - def get_by_order(self, db: Session, order_id: str) -> List[BizOrderStatusLog]: - """获取订单的状态变更日志""" - return db.query(BizOrderStatusLog).filter( - BizOrderStatusLog.order_id == order_id - ).order_by(BizOrderStatusLog.create_at.asc()).all() - - -class OrderAdjustCRUD(CRUDBase[BizOrderAdjust]): - """订单调整记录CRUD操作""" - - def get_by_order(self, db: Session, order_id: str) -> List[BizOrderAdjust]: - """获取订单的所有调整记录""" - return db.query(BizOrderAdjust).filter( - and_( - BizOrderAdjust.order_id == order_id, - BizOrderAdjust.is_del == 0 - ) - ).order_by(BizOrderAdjust.create_at.desc()).all() - - def get_by_order_item(self, db: Session, order_item_id: str) -> List[BizOrderAdjust]: - """获取订单项的调整记录""" - return db.query(BizOrderAdjust).filter( - and_( - BizOrderAdjust.order_item_id == order_item_id, - BizOrderAdjust.is_del == 0 - ) - ).order_by(BizOrderAdjust.create_at.desc()).all() - - def create_adjust_record( - self, - db: Session, - order_id: str, - adjust_type: str, - operator_id: str, - operator_name: str, - order_item_id: str = None, - before_quantity: Decimal = None, - after_quantity: Decimal = None, - before_amount: Decimal = None, - after_amount: Decimal = None, - difference_amount: Decimal = None, - reason: str = None, - substitute_product_id: str = None, - snapshot_items_json: Optional[str] = None, - ) -> BizOrderAdjust: - """创建调整记录""" - adjust_record = BizOrderAdjust( - id=generate_id(), - order_id=order_id, - order_item_id=order_item_id, - adjust_type=adjust_type, - before_quantity=before_quantity, - after_quantity=after_quantity, - before_amount=before_amount, - after_amount=after_amount, - difference_amount=difference_amount or Decimal("0"), - reason=reason, - substitute_product_id=substitute_product_id, - snapshot_items_json=snapshot_items_json, - operator_id=operator_id, - operator_name=operator_name, - create_by=operator_id, - create_name=operator_name - ) - db.add(adjust_record) - db.commit() - db.refresh(adjust_record) - return adjust_record - - -order_crud = OrderCRUD(BizOrder) -order_item_crud = OrderItemCRUD(BizOrderItem) -order_status_log_crud = OrderStatusLogCRUD(BizOrderStatusLog) -order_adjust_crud = OrderAdjustCRUD(BizOrderAdjust) diff --git a/backend/app/crud/order_adjust.py b/backend/app/crud/order_adjust.py deleted file mode 100644 index 0b934718..00000000 --- a/backend/app/crud/order_adjust.py +++ /dev/null @@ -1,148 +0,0 @@ -"""订单调整记录 CRUD""" -from typing import Optional, List, Dict, Any -from sqlalchemy.orm import Session -from sqlalchemy import and_ - -from app.core.utils import generate_id -from app.core.database import Base -from app.crud.base import CRUDBase -from app.models.models import BizOrderAdjust, BizOrderAdjustHis - - -class OrderAdjustCRUD(CRUDBase[BizOrderAdjust]): - """订单调整记录CRUD""" - - def __init__(self): - super().__init__(BizOrderAdjust) - - def create_adjust( - self, - db: Session, - order_id: str, - order_item_id: str, - adjust_type: str, - before_quantity: float, - after_quantity: float, - before_amount: float, - after_amount: float, - reason: str, - operator_id: str, - operator_name: str, - substitute_product_id: str = None, - create_by: str = None, - create_name: str = None - ) -> BizOrderAdjust: - """创建订单调整记录""" - data = { - "id": generate_id(), - "order_id": order_id, - "order_item_id": order_item_id, - "adjust_type": adjust_type, - "before_quantity": before_quantity, - "after_quantity": after_quantity, - "before_amount": before_amount, - "after_amount": after_amount, - "difference_amount": before_amount - after_amount, - "reason": reason, - "operator_id": operator_id, - "operator_name": operator_name, - "substitute_product_id": substitute_product_id, - "create_by": create_by or operator_id, - "create_name": create_name or operator_name - } - return self.create(db, data) - - def get_by_order(self, db: Session, order_id: str) -> List[BizOrderAdjust]: - """获取订单的所有调整记录""" - return db.query(self.model).filter( - and_( - self.model.order_id == order_id, - self.model.is_del == 0 - ) - ).order_by(self.model.create_at.desc()).all() - - def get_by_order_item(self, db: Session, order_item_id: str) -> List[BizOrderAdjust]: - """获取订单项的所有调整记录""" - return db.query(self.model).filter( - and_( - self.model.order_item_id == order_item_id, - self.model.is_del == 0 - ) - ).order_by(self.model.create_at.desc()).all() - - def get_total_adjust_amount(self, db: Session, order_id: str) -> float: - """获取订单的总调整金额(负数表示退款)""" - records = self.get_by_order(db, order_id) - return sum(r.difference_amount or 0 for r in records) - - def get_list( - self, - db: Session, - page: int = 1, - pageSize: int = 10, - keyword: Optional[str] = None, - adjust_type: Optional[str] = None, - start_date: Optional[str] = None, - end_date: Optional[str] = None - ) -> tuple: - """获取订单调整列表(支持分页和搜索)""" - query = db.query(self.model).filter(self.model.is_del == 0) - - # 关键词搜索(搜索订单号) - if keyword: - query = query.filter(self.model.order_id.like(f"%{keyword}%")) - - # 调整类型筛选 - if adjust_type: - query = query.filter(self.model.adjust_type == adjust_type) - - # 日期范围筛选 - if start_date: - query = query.filter(self.model.create_at >= start_date) - if end_date: - query = query.filter(self.model.create_at <= f"{end_date} 23:59:59") - - # 获取总数 - total = query.count() - - # 分页 - offset = (page - 1) * pageSize - records = query.order_by(self.model.create_at.desc()).offset(offset).limit(pageSize).all() - - return records, total - - def create_adjust_history( - self, - db: Session, - original_record: BizOrderAdjust, - operator_id: str, - operator_name: str - ) -> BizOrderAdjustHis: - """创建订单调整历史记录""" - from app.crud.base import HistoryCRUD - - history_crud = HistoryCRUD(BizOrderAdjustHis) - - data = { - "id": generate_id(), - "order_id": original_record.order_id, - "order_item_id": original_record.order_item_id, - "adjust_type": original_record.adjust_type, - "before_quantity": original_record.before_quantity, - "after_quantity": original_record.after_quantity, - "before_amount": original_record.before_amount, - "after_amount": original_record.after_amount, - "difference_amount": original_record.difference_amount, - "reason": original_record.reason, - "substitute_product_id": original_record.substitute_product_id, - "operator_id": operator_id, - "operator_name": operator_name, - "create_by": original_record.create_by, - "create_name": original_record.create_name - } - - return history_crud.create(db, data) - - -# 全局实例 -order_adjust_crud = OrderAdjustCRUD() diff --git a/backend/app/crud/password_history.py b/backend/app/crud/password_history.py deleted file mode 100644 index 8af00f7a..00000000 --- a/backend/app/crud/password_history.py +++ /dev/null @@ -1,44 +0,0 @@ -"""密码历史CRUD""" -from typing import Optional, List -from sqlalchemy.orm import Session - -from app.models.models import SysPasswordHistory -from app.crud.base import CRUDBase -from app.core.utils import generate_id - - -class PasswordHistoryCRUD(CRUDBase[SysPasswordHistory]): - """密码历史CRUD操作""" - - def create_password_history( - self, - db: Session, - user_id: str, - operation_type: str, - operator_id: str = None, - operator_name: str = None, - ip_address: str = None - ) -> SysPasswordHistory: - """创建密码修改历史""" - history = SysPasswordHistory( - id=generate_id(), - user_id=user_id, - operation_type=operation_type, - operator_id=operator_id, - operator_name=operator_name, - ip_address=ip_address - ) - db.add(history) - db.commit() - db.refresh(history) - return history - - def get_user_history(self, db: Session, user_id: str, limit: int = 5) -> List[SysPasswordHistory]: - """获取用户密码修改历史""" - return db.query(SysPasswordHistory).filter( - SysPasswordHistory.user_id == user_id, - SysPasswordHistory.is_del == 0 - ).order_by(SysPasswordHistory.create_at.desc()).limit(limit).all() - - -password_history_crud = PasswordHistoryCRUD(SysPasswordHistory) diff --git a/backend/app/crud/price_list.py b/backend/app/crud/price_list.py deleted file mode 100644 index 72e41821..00000000 --- a/backend/app/crud/price_list.py +++ /dev/null @@ -1,387 +0,0 @@ -"""价格清单CRUD""" -from typing import Optional, List -from datetime import datetime -from decimal import Decimal -from sqlalchemy.orm import Session -from sqlalchemy import and_, func - -from app.models.models import BizPriceList, BizPriceListItem, BizPriceListHis -from app.crud.base import CRUDBase -from app.core.utils import generate_id - - -class PriceListCRUD(CRUDBase[BizPriceList]): - """价格清单CRUD操作""" - - def get_by_region(self, db: Session, region_id: str) -> List[BizPriceList]: - """根据区域获取价格清单列表""" - return db.query(BizPriceList).filter( - and_( - BizPriceList.region_id == region_id, - BizPriceList.is_del == 0 - ) - ).order_by(BizPriceList.create_at.desc()).all() - - def get_active(self, db: Session, region_id: str) -> Optional[BizPriceList]: - """获取区域当前生效的价格清单""" - return db.query(BizPriceList).filter( - and_( - BizPriceList.region_id == region_id, - BizPriceList.status == "active", - BizPriceList.is_del == 0 - ) - ).first() - - def get_multi_with_filters( - self, - db: Session, - region_id: str = None, - supplier_id: str = None, - status: str = None, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizPriceList], int]: - """多条件查询价格清单""" - query = db.query(BizPriceList).filter(BizPriceList.is_del == 0) - - if region_id: - query = query.filter(BizPriceList.region_id == region_id) - if supplier_id: - query = query.filter(BizPriceList.supplier_id == supplier_id) - if status: - query = query.filter(BizPriceList.status == status) - - total = query.count() - price_lists = query.order_by(BizPriceList.create_at.desc()).offset(skip).limit(limit).all() - return price_lists, total - - def create_with_items( - self, - db: Session, - obj_in: dict, - items: List[dict], - creator: dict = None - ) -> BizPriceList: - """创建价格清单及其明细""" - # 计算版本号 - version = 1 - if obj_in.get("region_id"): - existing = db.query(BizPriceList).filter( - and_( - BizPriceList.region_id == obj_in["region_id"], - BizPriceList.is_del == 0 - ) - ).order_by(BizPriceList.version.desc()).first() - if existing: - version = existing.version + 1 - - price_list = BizPriceList( - id=generate_id(), - region_id=obj_in["region_id"], - supplier_id=obj_in["supplier_id"], - version=version, - status="draft", - effective_time=obj_in.get("effective_time"), - remark=obj_in.get("remark"), - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(price_list) - db.flush() - - # 创建价格明细 - for item in items: - price_item = BizPriceListItem( - id=generate_id(), - price_list_id=price_list.id, - product_id=item.get("product_id"), - unit_id=item.get("unit_id"), - price=Decimal(str(item.get("price", 0))), - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(price_item) - - db.commit() - db.refresh(price_list) - return price_list - - def update_with_items( - self, - db: Session, - id: str, - obj_in: dict, - items: List[dict] = None, - updater: dict = None - ) -> Optional[BizPriceList]: - """更新价格清单及其明细(仅草稿状态可编辑)""" - price_list = self.get(db, id) - if not price_list: - return None - - if price_list.status != "draft": - return None - - # 更新主表 - if obj_in.get("effective_time") is not None: - price_list.effective_time = obj_in["effective_time"] - if obj_in.get("remark") is not None: - price_list.remark = obj_in["remark"] - if updater: - price_list.update_by = updater.get("user_id") - - # 如果有新的价格项,则替换 - if items is not None: - # 删除旧的价格项 - db.query(BizPriceListItem).filter( - BizPriceListItem.price_list_id == id - ).delete() - - # 添加新的价格项 - for item in items: - price_item = BizPriceListItem( - id=generate_id(), - price_list_id=id, - product_id=item.get("product_id"), - unit_id=item.get("unit_id"), - price=Decimal(str(item.get("price", 0))), - create_by=updater.get("user_id") if updater else None, - create_name=updater.get("real_name") if updater else None - ) - db.add(price_item) - - db.commit() - db.refresh(price_list) - return price_list - - def publish( - self, - db: Session, - id: str, - publisher_id: str, - effective_time: datetime = None, - effective_type: str = "immediate" - ) -> Optional[BizPriceList]: - """发布价格清单""" - price_list = self.get(db, id) - if not price_list: - return None - - if price_list.status != "draft": - return None - - # 如果是立即生效,设为active状态 - if effective_type == "immediate": - # 先将其他生效中的价格清单设为失效 - existing_active = db.query(BizPriceList).filter( - and_( - BizPriceList.region_id == price_list.region_id, - BizPriceList.status == "active", - BizPriceList.id != id, - BizPriceList.is_del == 0 - ) - ).all() - for pl in existing_active: - pl.status = "inactive" - - price_list.status = "active" - price_list.effective_time = datetime.now() - else: - price_list.status = "pending" - if effective_time: - price_list.effective_time = effective_time - - price_list.published_by = publisher_id - price_list.published_time = datetime.now() - - db.commit() - db.refresh(price_list) - return price_list - - def invalidate( - self, - db: Session, - id: str - ) -> Optional[BizPriceList]: - """使价格清单失效""" - price_list = self.get(db, id) - if not price_list: - return None - - if price_list.status in ["active", "pending"]: - price_list.status = "inactive" - db.commit() - db.refresh(price_list) - - return price_list - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizPriceList]: - """更新价格清单并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "region_id": original.region_id, - "supplier_id": original.supplier_id, - "version": original.version, - "status": original.status, - "effective_time": original.effective_time, - "published_by": original.published_by, - "published_time": original.published_time, - "remark": original.remark, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizPriceListHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, id: str) -> bool: - """删除价格清单并记录历史""" - original = self.get(db, id) - if not original: - return False - - his_data = { - "id": original.id, - "region_id": original.region_id, - "supplier_id": original.supplier_id, - "version": original.version, - "status": original.status, - "effective_time": original.effective_time, - "published_by": original.published_by, - "published_time": original.published_time, - "remark": original.remark, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizPriceListHis(**his_data) - db.add(his) - - return self.delete(db, id) - - -class PriceListItemCRUD(CRUDBase[BizPriceListItem]): - """价格清单项CRUD操作""" - - def get_by_price_list(self, db: Session, price_list_id: str) -> List[BizPriceListItem]: - """获取价格清单的所有明细""" - return db.query(BizPriceListItem).filter( - and_( - BizPriceListItem.price_list_id == price_list_id, - BizPriceListItem.is_del == 0 - ) - ).all() - - def get_multi_by_price_lists( - self, - db: Session, - price_list_ids: List[str] - ) -> List[BizPriceListItem]: - """根据多个价格清单ID获取明细""" - return db.query(BizPriceListItem).filter( - and_( - BizPriceListItem.price_list_id.in_(price_list_ids), - BizPriceListItem.is_del == 0 - ) - ).all() - - def get_product_price( - self, - db: Session, - region_id: str, - product_id: str - ) -> Optional[BizPriceListItem]: - """获取当前生效价格清单中指定商品的价格""" - # 先获取当前生效的价格清单 - active_price_list = db.query(BizPriceList).filter( - and_( - BizPriceList.region_id == region_id, - BizPriceList.status == "active", - BizPriceList.is_del == 0 - ) - ).first() - - if not active_price_list: - return None - - return db.query(BizPriceListItem).filter( - and_( - BizPriceListItem.price_list_id == active_price_list.id, - BizPriceListItem.product_id == product_id, - BizPriceListItem.is_del == 0 - ) - ).first() - - def get_product_price_history( - self, - db: Session, - region_id: str, - product_id: str, - skip: int = 0, - limit: int = 20 - ) -> tuple[List[dict], int]: - """获取商品价格历史""" - # 获取所有生效过的价格清单及其明细 - price_lists = db.query(BizPriceList).filter( - and_( - BizPriceList.region_id == region_id, - BizPriceList.status.in_(["active", "inactive"]), - BizPriceList.is_del == 0 - ) - ).order_by(BizPriceList.published_time.desc()).all() - - price_list_ids = [pl.id for pl in price_lists] - - items = db.query(BizPriceListItem).filter( - and_( - BizPriceListItem.price_list_id.in_(price_list_ids), - BizPriceListItem.product_id == product_id, - BizPriceListItem.is_del == 0 - ) - ).all() - - # 关联价格清单信息 - result = [] - for item in items: - price_list = next((pl for pl in price_lists if pl.id == item.price_list_id), None) - if price_list: - result.append({ - "price": item.price, - "unit_id": item.unit_id, - "price_list_id": item.price_list_id, - "version": price_list.version, - "published_time": price_list.published_time, - "status": price_list.status - }) - - total = len(result) - return result[skip:skip+limit], total - - def get_by_product_and_price_list( - self, - db: Session, - product_id: str, - price_list_id: str - ) -> Optional[BizPriceListItem]: - """根据商品ID和价格清单ID获取价格项""" - return db.query(BizPriceListItem).filter( - and_( - BizPriceListItem.product_id == product_id, - BizPriceListItem.price_list_id == price_list_id, - BizPriceListItem.is_del == 0 - ) - ).first() - - -price_list_crud = PriceListCRUD(BizPriceList) -price_list_item_crud = PriceListItemCRUD(BizPriceListItem) diff --git a/backend/app/crud/product.py b/backend/app/crud/product.py deleted file mode 100644 index 2d25d333..00000000 --- a/backend/app/crud/product.py +++ /dev/null @@ -1,123 +0,0 @@ -"""商品CRUD""" -from typing import Optional, List -from sqlalchemy.orm import Session, defer -from sqlalchemy import and_, func, or_ - -from app.models.models import BasProduct, BasProductHis -from app.crud.base import CRUDBase - - -class ProductCRUD(CRUDBase[BasProduct]): - """商品CRUD操作""" - - def get_by_name(self, db: Session, name: str) -> Optional[BasProduct]: - """根据名称获取商品""" - return db.query(BasProduct).filter( - and_(BasProduct.name == name, BasProduct.is_del == 0) - ).first() - - def get_active_all(self, db: Session) -> List[BasProduct]: - """获取所有启用的商品""" - return db.query(BasProduct).filter( - and_(BasProduct.is_del == 0, BasProduct.is_active == True) - ).order_by(BasProduct.create_at.desc()).all() - - def get_multi_by_category(self, db: Session, category_id: str) -> List[BasProduct]: - """根据分类获取商品""" - return db.query(BasProduct).filter( - and_(BasProduct.category_id == category_id, BasProduct.is_del == 0, BasProduct.is_active == True) - ).order_by(BasProduct.create_at.desc()).all() - - def get_multi_with_filters( - self, - db: Session, - category_id: str = None, - keyword: str = None, - is_active: bool = None, - skip: int = 0, - limit: int = 10, - *, - defer_detail_content: bool = False, - ) -> tuple[List[BasProduct], int]: - """多条件查询商品""" - filters = [BasProduct.is_del == 0] - if category_id: - filters.append(BasProduct.category_id == category_id) - if is_active is not None: - filters.append(BasProduct.is_active == is_active) - if keyword: - filters.append( - or_( - BasProduct.name.like(f"%{keyword}%"), - BasProduct.description.like(f"%{keyword}%"), - ) - ) - - total = ( - db.query(func.count(BasProduct.id)) - .filter(*filters) - .order_by(None) - .scalar() - or 0 - ) - - query = db.query(BasProduct).filter(*filters) - if defer_detail_content: - query = query.options(defer(BasProduct.detail_content)) - products = query.order_by(BasProduct.create_at.desc()).offset(skip).limit(limit).all() - return products, total - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BasProduct]: - """更新商品并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "name": original.name, - "category_id": original.category_id, - "unit_id": original.unit_id, - "measure_value": getattr(original, "measure_value", None), - "description": original.description, - "image_url": original.image_url, - "is_active": original.is_active, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BasProductHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, id: str) -> bool: - """删除商品并记录历史""" - original = self.get(db, id) - if not original: - return False - - his_data = { - "id": original.id, - "name": original.name, - "category_id": original.category_id, - "unit_id": original.unit_id, - "measure_value": getattr(original, "measure_value", None), - "description": original.description, - "image_url": original.image_url, - "is_active": original.is_active, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BasProductHis(**his_data) - db.add(his) - - return self.delete(db, id) - - -product_crud = ProductCRUD(BasProduct) diff --git a/backend/app/crud/product_banner.py b/backend/app/crud/product_banner.py deleted file mode 100644 index 862663f4..00000000 --- a/backend/app/crud/product_banner.py +++ /dev/null @@ -1,73 +0,0 @@ -"""CRUD - 商品轮播图""" -from typing import List, Optional -from sqlalchemy.orm import Session - -from app.crud.base import CRUDBase, HistoryCRUD -from app.models.product_banner import BasProductBanner, BasProductBannerHis -from app.schemas.product_banner import ProductBannerCreate, ProductBannerUpdate - - -class ProductBannerCRUD(CRUDBase[BasProductBanner]): - """商品轮播图CRUD操作""" - - def get_by_product(self, db: Session, product_id: str) -> List[BasProductBanner]: - """获取商品的所有轮播图""" - return db.query(self.model).filter( - self.model.product_id == product_id, - self.model.is_del == 0 - ).order_by(self.model.sort_order.asc()).all() - - def create_batch( - self, - db: Session, - product_id: str, - items: List[dict], - creator: dict = None - ) -> List[BasProductBanner]: - """批量创建轮播图""" - banners = [] - for idx, item in enumerate(items): - item_data = { - "product_id": product_id, - "media_type": item.get("media_type", "image"), - "file_url": item.get("file_url", ""), - "thumbnail_url": item.get("thumbnail_url"), - "sort_order": item.get("sort_order", idx), - } - banner = self.create(db, item_data, creator) - banners.append(banner) - return banners - - def update_sort( - self, - db: Session, - items: List[dict] - ) -> bool: - """批量更新排序""" - for item in items: - banner_id = item.get("id") - sort_order = item.get("sort_order") - if banner_id and sort_order is not None: - db.query(self.model).filter(self.model.id == banner_id).update({ - "sort_order": sort_order - }) - db.commit() - return True - - def delete_by_product(self, db: Session, product_id: str) -> int: - """删除商品的所有轮播图""" - count = db.query(self.model).filter( - self.model.product_id == product_id, - self.model.is_del == 0 - ).update({"is_del": 1}) - db.commit() - return count - - -class ProductBannerHisCRUD(HistoryCRUD[BasProductBannerHis]): - """商品轮播图历史CRUD操作""" - pass - - -product_banner = ProductBannerCRUD(BasProductBanner) -product_banner_his = ProductBannerHisCRUD(BasProductBannerHis) diff --git a/backend/app/crud/product_detail_media.py b/backend/app/crud/product_detail_media.py deleted file mode 100644 index 1502517c..00000000 --- a/backend/app/crud/product_detail_media.py +++ /dev/null @@ -1,75 +0,0 @@ -"""CRUD - 商品详情媒体""" -from typing import List -from sqlalchemy.orm import Session - -from app.crud.base import CRUDBase, HistoryCRUD -from app.models.product_detail_media import BasProductDetailMedia, BasProductDetailMediaHis -from app.schemas.product_detail_media import ProductDetailMediaCreate, ProductDetailMediaUpdate - - -class ProductDetailMediaCRUD(CRUDBase[BasProductDetailMedia]): - """商品详情媒体CRUD操作""" - - def get_by_product(self, db: Session, product_id: str) -> List[BasProductDetailMedia]: - """获取商品的所有详情媒体""" - return db.query(self.model).filter( - self.model.product_id == product_id, - self.model.is_del == 0 - ).order_by(self.model.sort_order.asc()).all() - - def create_batch( - self, - db: Session, - product_id: str, - items: List[dict], - creator: dict = None - ) -> List[BasProductDetailMedia]: - """批量创建详情媒体""" - media_list = [] - for idx, item in enumerate(items): - item_data = { - "product_id": product_id, - "content_type": item.get("content_type", "image"), - "content_text": item.get("content_text"), - "file_url": item.get("file_url"), - "thumbnail_url": item.get("thumbnail_url"), - "description": item.get("description"), - "sort_order": item.get("sort_order", idx), - } - media = self.create(db, item_data, creator) - media_list.append(media) - return media_list - - def update_sort( - self, - db: Session, - items: List[dict] - ) -> bool: - """批量更新排序""" - for item in items: - media_id = item.get("id") - sort_order = item.get("sort_order") - if media_id and sort_order is not None: - db.query(self.model).filter(self.model.id == media_id).update({ - "sort_order": sort_order - }) - db.commit() - return True - - def delete_by_product(self, db: Session, product_id: str) -> int: - """删除商品的所有详情媒体""" - count = db.query(self.model).filter( - self.model.product_id == product_id, - self.model.is_del == 0 - ).update({"is_del": 1}) - db.commit() - return count - - -class ProductDetailMediaHisCRUD(HistoryCRUD[BasProductDetailMediaHis]): - """商品详情媒体历史CRUD操作""" - pass - - -product_detail_media = ProductDetailMediaCRUD(BasProductDetailMedia) -product_detail_media_his = ProductDetailMediaHisCRUD(BasProductDetailMediaHis) diff --git a/backend/app/crud/product_parameter.py b/backend/app/crud/product_parameter.py deleted file mode 100644 index d0eaead5..00000000 --- a/backend/app/crud/product_parameter.py +++ /dev/null @@ -1,83 +0,0 @@ -"""CRUD - 商品参数模板""" -from typing import List, Optional -from sqlalchemy.orm import Session -from sqlalchemy import or_ - -from app.crud.base import CRUDBase, HistoryCRUD -from app.models.product_parameter import BasProductParameter, BasProductParameterHis -from app.schemas.product_parameter import ProductParameterCreate, ProductParameterUpdate - - -class ProductParameterCRUD(CRUDBase[BasProductParameter]): - """商品参数模板CRUD操作""" - - def get_multi_paged( - self, - db: Session, - skip: int = 0, - limit: int = 10, - is_active: Optional[int] = None, - keyword: Optional[str] = None, - order_by: str = "sort_order", - order_type: str = "asc" - ) -> tuple: - """分页获取参数列表""" - query = db.query(self.model).filter(self.model.is_del == 0) - - if is_active is not None: - query = query.filter(self.model.is_active == is_active) - - if keyword: - query = query.filter( - or_( - self.model.name.like(f"%{keyword}%"), - self.model.code.like(f"%{keyword}%") - ) - ) - - total = query.count() - - order_column = getattr(self.model, order_by, None) - if order_column: - if order_type == "asc": - query = query.order_by(order_column.asc()) - else: - query = query.order_by(order_column.desc()) - else: - query = query.order_by(self.model.sort_order.asc()) - - items = query.offset(skip).limit(limit).all() - return items, total - - def get_active_all(self, db: Session) -> List[BasProductParameter]: - """获取所有启用的参数""" - return db.query(self.model).filter( - self.model.is_del == 0, - self.model.is_active == 1 - ).order_by(self.model.sort_order.asc()).all() - - def get_by_code(self, db: Session, code: str) -> Optional[BasProductParameter]: - """根据编码获取参数""" - return db.query(self.model).filter( - self.model.code == code, - self.model.is_del == 0 - ).first() - - def code_exists(self, db: Session, code: str, exclude_id: str = None) -> bool: - """检查编码是否已存在""" - query = db.query(self.model).filter( - self.model.code == code, - self.model.is_del == 0 - ) - if exclude_id: - query = query.filter(self.model.id != exclude_id) - return query.first() is not None - - -class ProductParameterHisCRUD(HistoryCRUD[BasProductParameterHis]): - """商品参数模板历史CRUD操作""" - pass - - -product_parameter = ProductParameterCRUD(BasProductParameter) -product_parameter_his = ProductParameterHisCRUD(BasProductParameterHis) diff --git a/backend/app/crud/product_parameter_value.py b/backend/app/crud/product_parameter_value.py deleted file mode 100644 index 1cd73433..00000000 --- a/backend/app/crud/product_parameter_value.py +++ /dev/null @@ -1,117 +0,0 @@ -"""CRUD - 商品参数值""" -from typing import List -from sqlalchemy.orm import Session -from sqlalchemy import and_ - -from app.crud.base import CRUDBase, HistoryCRUD -from app.models.product_parameter_value import BasProductParameterValue, BasProductParameterValueHis -from app.schemas.product_parameter_value import ProductParameterValueCreate - - -class ProductParameterValueCRUD(CRUDBase[BasProductParameterValue]): - """商品参数值CRUD操作""" - - def get_by_product(self, db: Session, product_id: str) -> List[BasProductParameterValue]: - """获取商品的所有参数值""" - return db.query(self.model).filter( - self.model.product_id == product_id, - self.model.is_del == 0 - ).order_by(self.model.sort_order.asc()).all() - - def get_by_product_with_detail(self, db: Session, product_id: str) -> List[dict]: - """获取商品的所有参数值(带参数详情)""" - from app.models.product_parameter import BasProductParameter - - results = db.query( - BasProductParameterValue, - BasProductParameter - ).join( - BasProductParameter, - and_( - BasProductParameterValue.parameter_id == BasProductParameter.id, - BasProductParameter.is_del == 0 - ) - ).filter( - BasProductParameterValue.product_id == product_id, - BasProductParameterValue.is_del == 0 - ).order_by( - BasProductParameterValue.sort_order.asc() - ).all() - - return [ - { - "id": ppv.id, - "product_id": ppv.product_id, - "parameter_id": ppv.parameter_id, - "parameter_name": pp.name, - "parameter_code": pp.code, - "param_type": pp.param_type, - "options": pp.options, - "placeholder": pp.placeholder, - "param_value": ppv.param_value, - "sort_order": ppv.sort_order, - } - for ppv, pp in results - ] - - def batch_save( - self, - db: Session, - product_id: str, - values: List[dict], - creator: dict = None - ) -> List[BasProductParameterValue]: - """批量保存商品参数值""" - from app.models.product_parameter import BasProductParameter - - # 先删除原有的参数值 - db.query(self.model).filter( - self.model.product_id == product_id, - self.model.is_del == 0 - ).update({"is_del": 1}) - - # 创建新的参数值 - saved_values = [] - for idx, value_item in enumerate(values): - parameter_id = value_item.get("parameter_id") - param_value = value_item.get("param_value") - - # 获取参数信息 - param = db.query(BasProductParameter).filter( - BasProductParameter.id == parameter_id, - BasProductParameter.is_del == 0 - ).first() - - if param: - item_data = { - "product_id": product_id, - "parameter_id": parameter_id, - "parameter_name": param.name, - "parameter_code": param.code, - "param_type": param.param_type, - "param_value": param_value, - "sort_order": idx, - } - pv = self.create(db, item_data, creator) - saved_values.append(pv) - - db.commit() - return saved_values - - def delete_by_product(self, db: Session, product_id: str) -> int: - """删除商品的所有参数值""" - count = db.query(self.model).filter( - self.model.product_id == product_id, - self.model.is_del == 0 - ).update({"is_del": 1}) - db.commit() - return count - - -class ProductParameterValueHisCRUD(HistoryCRUD[BasProductParameterValueHis]): - """商品参数值历史CRUD操作""" - pass - - -product_parameter_value = ProductParameterValueCRUD(BasProductParameterValue) -product_parameter_value_his = ProductParameterValueHisCRUD(BasProductParameterValueHis) diff --git a/backend/app/crud/product_review.py b/backend/app/crud/product_review.py deleted file mode 100644 index 389f8fcb..00000000 --- a/backend/app/crud/product_review.py +++ /dev/null @@ -1,242 +0,0 @@ -"""CRUD - 售后评价""" -from typing import List, Optional -from sqlalchemy.orm import Session -from sqlalchemy import and_, func - -from app.crud.base import CRUDBase, HistoryCRUD -from app.models.product_review import BizProductReview, BizProductReviewHis -from app.schemas.product_review import ProductReviewCreate, ProductReviewReplyRequest - - -class ProductReviewCRUD(CRUDBase[BizProductReview]): - """售后评价CRUD操作""" - - def get_by_product( - self, - db: Session, - product_id: str, - skip: int = 0, - limit: int = 10, - status: str = "approved", - has_media: int = None - ) -> tuple: - """获取商品的评价列表""" - query = db.query(self.model).filter( - self.model.product_id == product_id, - self.model.is_del == 0 - ) - - if status: - query = query.filter(self.model.status == status) - - if has_media == 1: - from app.models.product_review_media import BizProductReviewMedia - review_ids_with_media = db.query(BizProductReviewMedia.review_id).filter( - BizProductReviewMedia.is_del == 0 - ).distinct().subquery() - query = query.filter(self.model.id.in_(review_ids_with_media)) - - total = query.count() - - items = query.order_by( - self.model.is_top.desc(), - self.model.create_at.desc() - ).offset(skip).limit(limit).all() - - return items, total - - def get_admin_list( - self, - db: Session, - skip: int = 0, - limit: int = 10, - product_id: str = None, - product_name: str = None, - user_name: str = None, - status: str = None, - rating: int = None, - start_date: str = None, - end_date: str = None, - ) -> tuple: - """管理员获取评价列表(支持多条件筛选)""" - query = db.query(self.model).filter(self.model.is_del == 0) - - if product_id: - query = query.filter(self.model.product_id == product_id) - - if user_name: - query = query.filter(self.model.user_name.like(f"%{user_name}%")) - - if status: - query = query.filter(self.model.status == status) - - if rating: - query = query.filter(self.model.rating == rating) - - if start_date: - query = query.filter(self.model.create_at >= start_date) - - if end_date: - query = query.filter(self.model.create_at <= end_date) - - total = query.count() - - items = query.order_by( - self.model.is_top.desc(), - self.model.create_at.desc() - ).offset(skip).limit(limit).all() - - return items, total - - def get_statistics(self, db: Session, product_id: str = None) -> dict: - """获取评价统计""" - query = db.query(self.model).filter( - self.model.is_del == 0, - self.model.status == "approved" - ) - - if product_id: - query = query.filter(self.model.product_id == product_id) - - total_count = query.count() - - if total_count > 0: - avg_rating = db.query(func.avg(self.model.rating)).filter( - self.model.is_del == 0, - self.model.status == "approved", - self.model.product_id == product_id if product_id else True - ).scalar() or 0 - else: - avg_rating = 0 - - rating_dist = {} - for r in range(1, 6): - count = db.query(self.model).filter( - self.model.is_del == 0, - self.model.status == "approved", - self.model.rating == r, - self.model.product_id == product_id if product_id else True - ).count() - rating_dist[str(r)] = count - - pending_count = db.query(self.model).filter( - self.model.is_del == 0, - self.model.status == "pending", - self.model.product_id == product_id if product_id else True - ).count() - - rejected_count = db.query(self.model).filter( - self.model.is_del == 0, - self.model.status == "rejected", - self.model.product_id == product_id if product_id else True - ).count() - - return { - "total_count": total_count, - "pending_count": pending_count, - "approved_count": total_count, - "rejected_count": rejected_count, - "avg_rating": round(float(avg_rating), 1), - "rating_distribution": rating_dist, - } - - def reply( - self, - db: Session, - review_id: str, - reply_content: str, - updater: dict = None - ) -> Optional[BizProductReview]: - """回复评价""" - review = self.get(db, review_id) - if review: - review.reply_content = reply_content - review.reply_time = func.now() - if updater: - review.reply_by = updater.get("user_id") - review.reply_by_name = updater.get("real_name") - db.commit() - db.refresh(review) - return review - - def reject( - self, - db: Session, - review_id: str, - reject_reason: str, - updater: dict = None - ) -> Optional[BizProductReview]: - """拒绝评价""" - review = self.get(db, review_id) - if review: - review.status = "rejected" - review.reject_reason = reject_reason - db.commit() - db.refresh(review) - return review - - def approve( - self, - db: Session, - review_id: str - ) -> Optional[BizProductReview]: - """审核通过评价""" - review = self.get(db, review_id) - if review: - review.status = "approved" - db.commit() - db.refresh(review) - return review - - def toggle_top( - self, - db: Session, - review_id: str, - is_top: int - ) -> Optional[BizProductReview]: - """切换置顶状态""" - review = self.get(db, review_id) - if review: - review.is_top = is_top - db.commit() - db.refresh(review) - return review - - def like( - self, - db: Session, - review_id: str - ) -> Optional[BizProductReview]: - """点赞评价""" - review = self.get(db, review_id) - if review: - review.like_count = (review.like_count or 0) + 1 - db.commit() - db.refresh(review) - return review - - def user_can_review( - self, - db: Session, - user_id: str, - product_id: str, - order_id: str = None - ) -> bool: - """检查用户是否已评价过该商品""" - query = db.query(self.model).filter( - self.model.user_id == user_id, - self.model.product_id == product_id, - self.model.is_del == 0 - ) - if order_id: - query = query.filter(self.model.order_id == order_id) - return query.first() is not None - - -class ProductReviewHisCRUD(HistoryCRUD[BizProductReviewHis]): - """售后评价历史CRUD操作""" - pass - - -product_review = ProductReviewCRUD(BizProductReview) -product_review_his = ProductReviewHisCRUD(BizProductReviewHis) diff --git a/backend/app/crud/product_review_media.py b/backend/app/crud/product_review_media.py deleted file mode 100644 index a4a8af5d..00000000 --- a/backend/app/crud/product_review_media.py +++ /dev/null @@ -1,63 +0,0 @@ -"""CRUD - 评价媒体""" -from typing import List -from sqlalchemy.orm import Session - -from app.crud.base import CRUDBase, HistoryCRUD -from app.models.product_review_media import BizProductReviewMedia, BizProductReviewMediaHis - - -class ProductReviewMediaCRUD(CRUDBase[BizProductReviewMedia]): - """评价媒体CRUD操作""" - - def get_by_review(self, db: Session, review_id: str) -> List[BizProductReviewMedia]: - """获取评价的所有媒体""" - return db.query(self.model).filter( - self.model.review_id == review_id, - self.model.is_del == 0 - ).order_by(self.model.sort_order.asc()).all() - - def create_batch( - self, - db: Session, - review_id: str, - items: List[dict], - creator: dict = None - ) -> List[BizProductReviewMedia]: - """批量创建评价媒体""" - media_list = [] - for idx, item in enumerate(items): - item_data = { - "review_id": review_id, - "media_type": item.get("media_type", "image"), - "file_url": item.get("file_url", ""), - "thumbnail_url": item.get("thumbnail_url"), - "sort_order": item.get("sort_order", idx), - } - media = self.create(db, item_data, creator) - media_list.append(media) - return media_list - - def delete_by_review(self, db: Session, review_id: str) -> int: - """删除评价的所有媒体""" - count = db.query(self.model).filter( - self.model.review_id == review_id, - self.model.is_del == 0 - ).update({"is_del": 1}) - db.commit() - return count - - def get_media_count(self, db: Session, review_id: str) -> int: - """获取评价的媒体数量""" - return db.query(self.model).filter( - self.model.review_id == review_id, - self.model.is_del == 0 - ).count() - - -class ProductReviewMediaHisCRUD(HistoryCRUD[BizProductReviewMediaHis]): - """评价媒体历史CRUD操作""" - pass - - -product_review_media = ProductReviewMediaCRUD(BizProductReviewMedia) -product_review_media_his = ProductReviewMediaHisCRUD(BizProductReviewMediaHis) diff --git a/backend/app/crud/quotation.py b/backend/app/crud/quotation.py deleted file mode 100644 index 8d0bb7ca..00000000 --- a/backend/app/crud/quotation.py +++ /dev/null @@ -1,201 +0,0 @@ -"""报价单CRUD""" -from decimal import Decimal -from typing import Optional, List -from sqlalchemy.orm import Session -from sqlalchemy import and_, func - -from app.models.models import BizQuotation, BizQuotationItem, BizQuotationHis, BizQuotationItemHis -from app.crud.base import CRUDBase -from app.core.utils import generate_id - - -class QuotationCRUD(CRUDBase[BizQuotation]): - """报价单CRUD操作""" - - def get_by_inquiry(self, db: Session, inquiry_id: str) -> List[BizQuotation]: - """根据询价单获取报价列表""" - return db.query(BizQuotation).filter( - and_( - BizQuotation.inquiry_id == inquiry_id, - BizQuotation.is_del == 0 - ) - ).all() - - def get_by_supplier(self, db: Session, supplier_id: str) -> List[BizQuotation]: - """根据供应商获取报价列表""" - return db.query(BizQuotation).filter( - and_( - BizQuotation.supplier_id == supplier_id, - BizQuotation.is_del == 0 - ) - ).order_by(BizQuotation.create_at.desc()).all() - - def get_by_inquiry_and_supplier( - self, - db: Session, - inquiry_id: str, - supplier_id: str - ) -> Optional[BizQuotation]: - """根据询价单和供应商获取报价""" - return db.query(BizQuotation).filter( - and_( - BizQuotation.inquiry_id == inquiry_id, - BizQuotation.supplier_id == supplier_id, - BizQuotation.is_del == 0 - ) - ).first() - - def get_latest_by_inquiry_and_supplier( - self, - db: Session, - inquiry_id: str, - supplier_id: str - ) -> Optional[BizQuotation]: - """获取某供应商在某询价单下最新一次报价""" - return db.query(BizQuotation).filter( - and_( - BizQuotation.inquiry_id == inquiry_id, - BizQuotation.supplier_id == supplier_id, - BizQuotation.is_del == 0 - ) - ).order_by(BizQuotation.create_at.desc()).first() - - def get_multi_with_filters( - self, - db: Session, - inquiry_id: str = None, - supplier_id: str = None, - region_id: str = None, - status: str = None, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[BizQuotation], int]: - """多条件查询报价单""" - query = db.query(BizQuotation).filter(BizQuotation.is_del == 0) - - if inquiry_id: - query = query.filter(BizQuotation.inquiry_id == inquiry_id) - if supplier_id: - query = query.filter(BizQuotation.supplier_id == supplier_id) - if region_id: - query = query.filter(BizQuotation.region_id == region_id) - if status: - query = query.filter(BizQuotation.status == status) - - total = query.count() - quotations = query.order_by(BizQuotation.create_at.desc()).offset(skip).limit(limit).all() - return quotations, total - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizQuotation]: - """更新报价单并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "inquiry_id": original.inquiry_id, - "supplier_id": original.supplier_id, - "region_id": original.region_id, - "status": original.status, - "total_amount": original.total_amount, - "submit_time": original.submit_time, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizQuotationHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, id: str) -> bool: - """删除报价单并记录历史""" - original = self.get(db, id) - if not original: - return False - - his_data = { - "id": original.id, - "inquiry_id": original.inquiry_id, - "supplier_id": original.supplier_id, - "region_id": original.region_id, - "status": original.status, - "total_amount": original.total_amount, - "submit_time": original.submit_time, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizQuotationHis(**his_data) - db.add(his) - - return self.delete(db, id) - - -class QuotationItemCRUD(CRUDBase[BizQuotationItem]): - """报价单项CRUD操作""" - - def get_by_quotation(self, db: Session, quotation_id: str) -> List[BizQuotationItem]: - """获取报价单的所有报价项""" - return db.query(BizQuotationItem).filter( - and_( - BizQuotationItem.quotation_id == quotation_id, - BizQuotationItem.is_del == 0 - ) - ).all() - - def get_multi_by_quotations( - self, - db: Session, - quotation_ids: List[str] - ) -> List[BizQuotationItem]: - """根据多个报价单ID获取报价项""" - return db.query(BizQuotationItem).filter( - and_( - BizQuotationItem.quotation_id.in_(quotation_ids), - BizQuotationItem.is_del == 0 - ) - ).all() - - def create_items( - self, - db: Session, - quotation_id: str, - items: List[dict], - creator: dict = None - ) -> List[BizQuotationItem]: - """批量创建报价项""" - result = [] - for item in items: - quotation_item = BizQuotationItem( - id=generate_id(), - quotation_id=quotation_id, - product_id=item.get("product_id"), - unit_id=item.get("unit_id"), - price=Decimal(str(item.get("price", 0))), - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(quotation_item) - result.append(quotation_item) - db.commit() - for item in result: - db.refresh(item) - return result - - def delete_by_quotation(self, db: Session, quotation_id: str) -> bool: - """删除报价单的所有报价项""" - items = self.get_by_quotation(db, quotation_id) - for item in items: - item.is_del = 1 - db.commit() - return True - - -quotation_crud = QuotationCRUD(BizQuotation) -quotation_item_crud = QuotationItemCRUD(BizQuotationItem) diff --git a/backend/app/crud/region.py b/backend/app/crud/region.py deleted file mode 100644 index ee5f785d..00000000 --- a/backend/app/crud/region.py +++ /dev/null @@ -1,81 +0,0 @@ -"""区域管理CRUD""" -from typing import Optional, List -from sqlalchemy.orm import Session -from sqlalchemy import and_ - -from app.models.models import SysRegion, SysRegionHis -from app.crud.base import CRUDBase - - -class RegionCRUD(CRUDBase[SysRegion]): - """区域CRUD操作""" - - def get_by_code(self, db: Session, code: str) -> Optional[SysRegion]: - """根据编码获取区域(含已删除,匹配数据库唯一索引约束)""" - return db.query(SysRegion).filter(SysRegion.code == code).first() - - def get_active_all(self, db: Session) -> List[SysRegion]: - """获取所有启用的区域""" - return db.query(SysRegion).filter( - and_(SysRegion.is_del == 0, SysRegion.is_active == True) - ).order_by(SysRegion.sort_order.asc()).all() - - def create_with_history(self, db: Session, obj_in: dict, creator: dict = None) -> SysRegion: - """创建区域并记录历史""" - obj = self.create(db, obj_in, creator) - return obj - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[SysRegion]: - """更新区域并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "name": original.name, - "code": original.code, - "description": original.description, - "sort_order": original.sort_order, - "expected_delivery_minutes": original.expected_delivery_minutes, - "delivery_warn_advance_minutes": original.delivery_warn_advance_minutes, - "is_active": original.is_active, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = SysRegionHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, id: str) -> bool: - """删除区域并记录历史""" - original = self.get(db, id) - if not original: - return False - - his_data = { - "id": original.id, - "name": original.name, - "code": original.code, - "description": original.description, - "sort_order": original.sort_order, - "expected_delivery_minutes": original.expected_delivery_minutes, - "delivery_warn_advance_minutes": original.delivery_warn_advance_minutes, - "is_active": original.is_active, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = SysRegionHis(**his_data) - db.add(his) - - return self.delete(db, id) - - -region_crud = RegionCRUD(SysRegion) diff --git a/backend/app/crud/settlement.py b/backend/app/crud/settlement.py deleted file mode 100644 index 18669c7b..00000000 --- a/backend/app/crud/settlement.py +++ /dev/null @@ -1,604 +0,0 @@ -"""结算与退款CRUD""" -from typing import Optional, List, Tuple -from datetime import datetime -from decimal import Decimal -from sqlalchemy.orm import Session -from sqlalchemy import and_, func - -from app.models.models import BizSettlement, BizSettlementHis, BizRefund, BizRefundHis, BizOrder -from app.crud.base import CRUDBase -from app.crud.order import order_crud -from app.core.utils import generate_id -from app.core.order_status_compat import derive_legacy_status_from_three - - -class SettlementCRUD(CRUDBase[BizSettlement]): - """结算记录CRUD操作""" - - def get_multi_with_filters( - self, - db: Session, - region_id: str = None, - client_id: str = None, - status: str = None, - start_date: str = None, - end_date: str = None, - skip: int = 0, - limit: int = 10 - ) -> Tuple[List[BizSettlement], int]: - """多条件查询结算记录""" - query = db.query(BizSettlement).filter(BizSettlement.is_del == 0) - - if region_id: - query = query.filter(BizSettlement.region_id == region_id) - if client_id: - query = query.filter(BizSettlement.client_id == client_id) - if status: - query = query.filter(BizSettlement.status == status) - if start_date: - query = query.filter(BizSettlement.create_at >= start_date) - if end_date: - query = query.filter(BizSettlement.create_at <= end_date) - - total = query.count() - settlements = query.order_by(BizSettlement.create_at.desc()).offset(skip).limit(limit).all() - return settlements, total - - def get_by_order(self, db: Session, order_id: str) -> List[BizSettlement]: - """根据订单获取结算记录""" - return db.query(BizSettlement).filter( - and_( - BizSettlement.order_id == order_id, - BizSettlement.is_del == 0 - ) - ).all() - - def create_settlement( - self, - db: Session, - order_id: str, - client_id: str, - region_id: str, - amount: Decimal, - settlement_method: str, - settlement_time: datetime = None, - attachment_url: str = None, - note: str = None, - creator: dict = None - ) -> BizSettlement: - """创建结算记录""" - settlement = BizSettlement( - id=generate_id(), - order_id=order_id, - client_id=client_id, - region_id=region_id, - amount=amount, - settlement_method=settlement_method, - settlement_time=settlement_time or datetime.now(), - settlement_file_url=attachment_url, - settlement_note=note, - status="confirmed", - reviewed_by=creator.get("user_id") if creator else None, - reviewed_time=datetime.now(), - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(settlement) - db.flush() - - # 更新订单的已付金额 - order = db.query(BizOrder).filter(BizOrder.id == order_id).first() - if order: - order.paid_amount = (order.paid_amount or Decimal("0")) + amount - if order.paid_amount >= order.receivable_amount: - order.paid_amount = order.receivable_amount - order.update_at = datetime.now() - order_crud.sync_pay_fields_from_amounts(order) - - db.commit() - db.refresh(settlement) - return settlement - - def batch_create_settlements( - self, - db: Session, - order_ids: List[str], - settlement_method: str, - settlement_time: datetime = None, - attachment_url: str = None, - note: str = None, - creator: dict = None - ) -> List[BizSettlement]: - """批量创建结算记录""" - settlements = [] - for order_id in order_ids: - order = db.query(BizOrder).filter( - and_( - BizOrder.id == order_id, - BizOrder.is_del == 0 - ) - ).first() - - if order and order.status not in ["cancelled", "completed"]: - # 计算未付金额 - unpaid_amount = (order.receivable_amount or Decimal("0")) - (order.paid_amount or Decimal("0")) - - settlement = BizSettlement( - id=generate_id(), - order_id=order_id, - client_id=order.client_id, - region_id=order.region_id, - amount=unpaid_amount, - settlement_method=settlement_method, - settlement_time=settlement_time or datetime.now(), - settlement_file_url=attachment_url, - settlement_note=note, - status="confirmed", - reviewed_by=creator.get("user_id") if creator else None, - reviewed_time=datetime.now(), - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(settlement) - settlements.append(settlement) - - # 更新订单的已付金额 - order.paid_amount = order.receivable_amount - order.update_at = datetime.now() - order_crud.sync_pay_fields_from_amounts(order) - - db.commit() - return settlements - - def confirm_settlement( - self, - db: Session, - settlement_id: str, - reviewer_id: str, - note: str = None - ) -> Optional[BizSettlement]: - """确认结算""" - settlement = self.get(db, settlement_id) - if not settlement: - return None - - if settlement.status != "pending": - return None - - settlement.status = "confirmed" - settlement.reviewed_by = reviewer_id - settlement.reviewed_time = datetime.now() - if note: - settlement.settlement_note = note - settlement.update_at = datetime.now() - - # 更新订单的已付金额 - order = db.query(BizOrder).filter(BizOrder.id == settlement.order_id).first() - if order: - order.paid_amount = (order.paid_amount or Decimal("0")) + settlement.amount - if order.paid_amount >= order.receivable_amount: - order.paid_amount = order.receivable_amount - order.update_at = datetime.now() - order_crud.sync_pay_fields_from_amounts(order) - - db.commit() - db.refresh(settlement) - return settlement - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizSettlement]: - """更新结算记录并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "order_id": original.order_id, - "client_id": original.client_id, - "region_id": original.region_id, - "amount": original.amount, - "settlement_method": original.settlement_method, - "settlement_time": original.settlement_time, - "settlement_file_url": original.settlement_file_url, - "settlement_note": original.settlement_note, - "status": original.status, - "reviewed_by": original.reviewed_by, - "reviewed_time": original.reviewed_time, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizSettlementHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def get_client_summary( - self, - db: Session, - region_id: str = None, - client_id: str = None, - skip: int = 0, - limit: int = 10 - ) -> Tuple[List[dict], int]: - """获取客户待结算汇总""" - query = db.query( - BizOrder.client_id, - func.count(BizOrder.id).label("order_count"), - func.sum(BizOrder.receivable_amount - BizOrder.paid_amount).label("total_unpaid") - ).filter( - and_( - BizOrder.is_del == 0, - BizOrder.receivable_amount > BizOrder.paid_amount, - BizOrder.status.notin_(["cancelled"]) - ) - ) - - if region_id: - query = query.filter(BizOrder.region_id == region_id) - if client_id: - query = query.filter(BizOrder.client_id == client_id) - - query = query.group_by(BizOrder.client_id) - - total = query.count() - results = query.offset(skip).limit(limit).all() - - return [{"client_id": r.client_id, "order_count": r.order_count, "total_unpaid": r.total_unpaid or Decimal("0")} for r in results], total - - def get_statistics( - self, - db: Session, - region_id: str = None, - start_date: str = None, - end_date: str = None - ) -> dict: - """获取结算统计数据""" - # 待结算金额(使用单条SQL查询,避免N+1问题) - pending_query = db.query( - func.count(BizOrder.id).label("order_count"), - func.coalesce(func.sum(BizOrder.receivable_amount - BizOrder.paid_amount), 0).label("pending_amount") - ).filter( - and_( - BizOrder.is_del == 0, - BizOrder.receivable_amount > BizOrder.paid_amount, - BizOrder.status.notin_(["cancelled", "completed"]) - ) - ) - if region_id: - pending_query = pending_query.filter(BizOrder.region_id == region_id) - pending_result = pending_query.first() - - # 已结算金额 - settled_query = db.query( - func.count(BizSettlement.id).label("settlement_count"), - func.coalesce(func.sum(BizSettlement.amount), 0).label("settled_amount") - ).filter( - and_( - BizSettlement.is_del == 0, - BizSettlement.status == "confirmed" - ) - ) - if region_id: - settled_query = settled_query.filter(BizSettlement.region_id == region_id) - if start_date: - settled_query = settled_query.filter(BizSettlement.reviewed_time >= start_date) - if end_date: - settled_query = settled_query.filter(BizSettlement.reviewed_time <= end_date) - settled_result = settled_query.first() - - # 待处理退款数 - refund_pending_count = db.query(func.count(BizRefund.id)).filter( - and_( - BizRefund.is_del == 0, - BizRefund.status.in_(["pending", "processing"]) - ) - ) - if region_id: - refund_pending_count = refund_pending_count.join( - BizOrder, BizOrder.id == BizRefund.order_id - ).filter(BizOrder.region_id == region_id) - refund_pending_count = refund_pending_count.scalar() or 0 - - return { - "pendingOrderCount": pending_result.order_count or 0, - "pendingAmount": float(pending_result.pending_amount or 0), - "settledCount": settled_result.settlement_count or 0, - "settledAmount": float(settled_result.settled_amount or 0), - "refundPendingCount": refund_pending_count - } - - -class RefundCRUD(CRUDBase[BizRefund]): - """退款记录CRUD操作""" - - def get_multi_with_filters( - self, - db: Session, - region_id: str = None, - order_id: str = None, - order_no: str = None, - status: str = None, - refund_method: str = None, - start_date: str = None, - end_date: str = None, - skip: int = 0, - limit: int = 10 - ) -> Tuple[List[BizRefund], int]: - """多条件查询退款记录""" - query = db.query(BizRefund).filter(BizRefund.is_del == 0) - - if region_id: - # 需要关联订单获取区域 - query = query.join(BizOrder, BizOrder.id == BizRefund.order_id).filter( - BizOrder.region_id == region_id - ) - if order_id: - query = query.filter(BizRefund.order_id == order_id) - if order_no: - query = query.join(BizOrder, BizOrder.id == BizRefund.order_id).filter( - BizOrder.order_no.like(f"%{order_no}%") - ) - if status: - query = query.filter(BizRefund.status == status) - if refund_method: - query = query.filter(BizRefund.refund_method == refund_method) - if start_date: - query = query.filter(BizRefund.create_at >= start_date) - if end_date: - query = query.filter(BizRefund.create_at <= end_date) - - total = query.count() - refunds = query.order_by(BizRefund.create_at.desc()).offset(skip).limit(limit).all() - return refunds, total - - def get_by_order(self, db: Session, order_id: str) -> List[BizRefund]: - """根据订单获取退款记录""" - return db.query(BizRefund).filter( - and_( - BizRefund.order_id == order_id, - BizRefund.is_del == 0 - ) - ).all() - - def get_by_refund_no(self, db: Session, refund_no: str) -> Optional[BizRefund]: - """根据退款单号获取退款记录""" - return db.query(BizRefund).filter( - and_( - BizRefund.refund_no == refund_no, - BizRefund.is_del == 0 - ) - ).first() - - def create_refund( - self, - db: Session, - order_id: str, - refund_amount: Decimal, - refund_method: str, - reason: str = None, - creator: dict = None - ) -> BizRefund: - """创建退款记录""" - refund_no = self._generate_refund_no(db) - - refund = BizRefund( - id=generate_id(), - order_id=order_id, - refund_no=refund_no, - refund_amount=refund_amount, - refund_method=refund_method, - refund_result="processing", - status="pending", - create_by=creator.get("user_id") if creator else None, - create_name=creator.get("real_name") if creator else None - ) - db.add(refund) - - # 更新订单的退款金额和状态 - order = db.query(BizOrder).filter(BizOrder.id == order_id).first() - if order: - order.refund_amount = (order.refund_amount or Decimal("0")) + refund_amount - order.update_at = datetime.now() - - # 如果是微信退款,更新订单状态为退款中 - if refund_method == "wechat": - order.pay_status = "refunding" - order.status = derive_legacy_status_from_three( - order_status=order.order_status, - delivery_status=order.delivery_status, - pay_status=order.pay_status, - ) - - db.commit() - db.refresh(refund) - return refund - - def process_refund( - self, - db: Session, - refund_id: str, - refund_result: str, - refund_no_wechat: str = None, - updater: dict = None - ) -> Optional[BizRefund]: - """处理退款(成功/失败)""" - refund = self.get(db, refund_id) - if not refund: - return None - - refund.refund_result = refund_result - refund.refund_time = datetime.now() - if refund_no_wechat: - refund.refund_no_wechat = refund_no_wechat - - if refund_result == "success": - refund.status = "completed" - # 更新订单退款金额 - order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first() - if order: - order.refund_amount = (order.refund_amount or Decimal("0")) + refund.refund_amount - order.paid_amount = max(Decimal("0"), (order.paid_amount or Decimal("0")) - refund.refund_amount) - order.pay_status = "refunded" if (order.paid_amount or Decimal("0")) <= Decimal("0") else "paid" - order.status = derive_legacy_status_from_three( - order_status=order.order_status, - delivery_status=order.delivery_status, - pay_status=order.pay_status, - ) - order.update_at = datetime.now() - - # 如果退款金额等于已支付金额,订单变为已取消 - if order.refund_amount >= (order.total_amount - order.paid_amount + order.refund_amount): - # 保留部分退款状态 - pass - else: - refund.status = "failed" - # 恢复订单状态 - order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first() - if order and order.pay_status == "refunding": - order.pay_status = "paid" if (order.paid_amount or Decimal("0")) > Decimal("0") else "pending_payment" - order.status = derive_legacy_status_from_three( - order_status=order.order_status, - delivery_status=order.delivery_status, - pay_status=order.pay_status, - ) - - refund.update_at = datetime.now() - db.commit() - db.refresh(refund) - return refund - - def handle_refund_failure( - self, - db: Session, - refund_id: str, - fail_reason: str - ) -> Optional[BizRefund]: - """处理退款失败""" - refund = self.get(db, refund_id) - if not refund: - return None - - refund.refund_result = "failed" - refund.refund_fail_reason = fail_reason - refund.refund_time = datetime.now() - refund.status = "failed" - refund.update_at = datetime.now() - - # 恢复订单状态 - order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first() - if order and order.pay_status == "refunding": - order.pay_status = "paid" if (order.paid_amount or Decimal("0")) > Decimal("0") else "pending_payment" - order.status = derive_legacy_status_from_three( - order_status=order.order_status, - delivery_status=order.delivery_status, - pay_status=order.pay_status, - ) - order.update_at = datetime.now() - - db.commit() - db.refresh(refund) - return refund - - def _generate_refund_no(self, db: Session) -> str: - """生成退款单号""" - today = datetime.now().strftime("%Y%m%d") - prefix = f"REF{today}" - - last_refund = db.query(BizRefund).filter( - BizRefund.refund_no.like(f"{prefix}%") - ).order_by(BizRefund.refund_no.desc()).first() - - if last_refund: - last_no = int(last_refund.refund_no[-4:]) - new_no = last_no + 1 - else: - new_no = 1 - - return f"{prefix}{str(new_no).zfill(4)}" - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizRefund]: - """更新退款记录并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "order_id": original.order_id, - "refund_no": original.refund_no, - "refund_amount": original.refund_amount, - "refund_method": original.refund_method, - "refund_no_wechat": original.refund_no_wechat, - "refund_time": original.refund_time, - "refund_result": original.refund_result, - "refund_fail_reason": original.refund_fail_reason, - "status": original.status, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BizRefundHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def get_refund_statistics( - self, - db: Session, - start_date: str = None, - end_date: str = None - ) -> dict: - """获取退款统计数据""" - base_query = db.query(BizRefund).filter( - and_( - BizRefund.is_del == 0, - BizRefund.status == "completed" - ) - ) - - if start_date: - base_query = base_query.filter(BizRefund.refund_time >= start_date) - if end_date: - base_query = base_query.filter(BizRefund.refund_time <= end_date) - - # 成功退款金额 - success_amount = base_query.filter( - BizRefund.refund_result == "success" - ).func.sum(BizRefund.refund_amount).scalar() or Decimal("0") - - # 失败退款金额 - failed_amount = base_query.filter( - BizRefund.refund_result == "failed" - ).func.sum(BizRefund.refund_amount).scalar() or Decimal("0") - - # 待处理退款数 - pending_count = db.query(func.count(BizRefund.id)).filter( - and_( - BizRefund.is_del == 0, - BizRefund.status.in_(["pending", "processing"]) - ) - ).scalar() or 0 - - # 退款失败数 - failed_count = db.query(func.count(BizRefund.id)).filter( - and_( - BizRefund.is_del == 0, - BizRefund.status == "failed" - ) - ).scalar() or 0 - - return { - "successAmount": float(success_amount), - "failedAmount": float(failed_amount), - "pendingCount": pending_count, - "failedCount": failed_count - } - - -settlement_crud = SettlementCRUD(BizSettlement) -refund_crud = RefundCRUD(BizRefund) diff --git a/backend/app/crud/statistics.py b/backend/app/crud/statistics.py deleted file mode 100644 index 1ffe41d9..00000000 --- a/backend/app/crud/statistics.py +++ /dev/null @@ -1,916 +0,0 @@ -"""数据统计 CRUD""" -from sqlalchemy.orm import Session -from sqlalchemy import and_, func, desc, extract -from typing import Optional, List, Dict, Any, Tuple -from datetime import datetime, timedelta -from decimal import Decimal - -from app.models.models import ( - BizOrder, BizOrderItem, BizDeliveryTask, - BizSettlement, BizRefund, BizPriceList, BizPriceListItem, - SysUser, SysRegion, BasProduct, - BizInquiry, BizQuotation, BizDeliveryWarning, -) - - -class StatisticsCRUD: - """数据统计CRUD操作""" - - @staticmethod - def get_order_statistics( - db: Session, - region_id: str = None, - start_date: str = None, - end_date: str = None - ) -> Dict[str, Any]: - """获取订单统计数据""" - query = db.query(BizOrder).filter(BizOrder.is_del == 0) - - if region_id: - query = query.filter(BizOrder.region_id == region_id) - - if start_date: - try: - start_dt = datetime.strptime(start_date, "%Y-%m-%d") - query = query.filter(BizOrder.create_at >= start_dt) - except: - pass - - if end_date: - try: - end_dt = datetime.strptime(end_date, "%Y-%m-%d") - end_dt = end_dt.replace(hour=23, minute=59, second=59) - query = query.filter(BizOrder.create_at <= end_dt) - except: - pass - - orders = query.all() - - total_orders = len(orders) - pending_payment = sum(1 for o in orders if o.status == "pending_payment") - pending_dispatch = sum(1 for o in orders if getattr(o, "delivery_status", None) == "pending_dispatch") - delivering = sum(1 for o in orders if o.status == "delivering") - completed = sum(1 for o in orders if o.status == "completed") - cancelled = sum(1 for o in orders if o.status == "cancelled") - - total_amount = sum(float(o.total_amount or 0) for o in orders) - paid_amount = sum(float(o.paid_amount or 0) for o in orders) - refund_amount = sum(float(o.refund_amount or 0) for o in orders) - - return { - "totalOrders": total_orders, - "pendingPayment": pending_payment, - "pendingDelivery": pending_dispatch, - "delivering": delivering, - "completed": completed, - "cancelled": cancelled, - "totalAmount": round(total_amount, 2), - "paidAmount": round(paid_amount, 2), - "refundAmount": round(refund_amount, 2) - } - - @staticmethod - def get_order_trend( - db: Session, - region_id: str = None, - days: int = 7, - include_inactive_regions: bool = False, - ) -> List[Dict[str, Any]]: - """获取订单趋势数据(按日统计)""" - result = [] - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True] - - for i in range(days - 1, -1, -1): - day_start = today - timedelta(days=i) - day_end = day_start + timedelta(days=1) - - query = ( - db.query(BizOrder) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter( - and_( - BizOrder.is_del == 0, - BizOrder.create_at >= day_start, - BizOrder.create_at < day_end, - SysRegion.is_del == 0, - *region_active_clause, - ) - ) - ) - - if region_id: - query = query.filter(BizOrder.region_id == region_id) - - orders = query.all() - order_count = len(orders) - order_amount = sum(float(o.total_amount or 0) for o in orders) - - result.append({ - "date": day_start.strftime("%Y-%m-%d"), - "orderCount": order_count, - "orderAmount": round(order_amount, 2) - }) - - return result - - @staticmethod - def get_today_order_stats_by_region( - db: Session, - region_id: str = None, - include_inactive_regions: bool = False, - ) -> List[Dict[str, Any]]: - """今日各区域营业额(元)、订单笔数;列出未删除的区域,无单则为 0。""" - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - day_end = today + timedelta(days=1) - region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True] - - reg_q = db.query(SysRegion).filter( - SysRegion.is_del == 0, - ) - if region_id: - reg_q = reg_q.filter(SysRegion.id == region_id) - regions = reg_q.order_by(SysRegion.sort_order, SysRegion.name).all() - - agg_q = ( - db.query( - BizOrder.region_id, - func.count(BizOrder.id).label("order_count"), - func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"), - ) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter( - BizOrder.is_del == 0, - BizOrder.create_at >= today, - BizOrder.create_at < day_end, - SysRegion.is_del == 0, - *region_active_clause, - ) - .group_by(BizOrder.region_id) - ) - if region_id: - agg_q = agg_q.filter(BizOrder.region_id == region_id) - agg_map: Dict[str, Any] = {r.region_id: r for r in agg_q.all() if r.region_id} - - out: List[Dict[str, Any]] = [] - for reg in regions: - row = agg_map.get(reg.id) - cnt = int(row.order_count or 0) if row else 0 - amt = float(row.order_amount or 0) if row else 0.0 - out.append({ - "regionId": reg.id, - "regionName": reg.name or reg.id, - "orderCount": cnt, - "orderAmount": round(amt, 2), - }) - return out - - @staticmethod - def get_today_order_stats_by_client( - db: Session, - region_id: str = None, - limit: int = 30, - include_inactive_regions: bool = False, - ) -> List[Dict[str, Any]]: - """今日各下单客户:下单金额合计(元)、商品数量合计(订单行数量之和);按金额降序,最多 limit 条。""" - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - day_end = today + timedelta(days=1) - region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True] - - amt_q = ( - db.query( - BizOrder.client_id, - func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"), - ) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter( - BizOrder.is_del == 0, - BizOrder.create_at >= today, - BizOrder.create_at < day_end, - SysRegion.is_del == 0, - *region_active_clause, - ) - ) - if region_id: - amt_q = amt_q.filter(BizOrder.region_id == region_id) - amt_rows = amt_q.group_by(BizOrder.client_id).all() - - qty_q = ( - db.query( - BizOrder.client_id, - func.coalesce(func.sum(BizOrderItem.quantity), 0).label("qty"), - ) - .select_from(BizOrderItem) - .join( - BizOrder, - and_( - BizOrder.id == BizOrderItem.order_id, - BizOrder.is_del == 0, - BizOrder.create_at >= today, - BizOrder.create_at < day_end, - ), - ) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter(BizOrderItem.is_del == 0) - .filter(SysRegion.is_del == 0, *region_active_clause) - ) - if region_id: - qty_q = qty_q.filter(BizOrder.region_id == region_id) - qty_rows = qty_q.group_by(BizOrder.client_id).all() - qty_map: Dict[str, float] = { - r.client_id: float(r.qty or 0) for r in qty_rows if r.client_id - } - - merged: List[Dict[str, Any]] = [] - for r in amt_rows: - cid = r.client_id - if not cid: - continue - merged.append({ - "clientId": cid, - "orderAmount": round(float(r.order_amount or 0), 2), - "productQuantity": round(float(qty_map.get(cid, 0) or 0), 4), - }) - - merged.sort(key=lambda x: -x["orderAmount"]) - merged = merged[: max(1, min(int(limit or 30), 100))] - - client_ids = [m["clientId"] for m in merged] - names: Dict[str, str] = {} - if client_ids: - for u in ( - db.query(SysUser) - .filter(and_(SysUser.is_del == 0, SysUser.id.in_(client_ids))) - .all() - ): - names[u.id] = (u.real_name or u.phone or u.username or u.id) or u.id - - out: List[Dict[str, Any]] = [] - for m in merged: - cid = m["clientId"] - nm = names.get(cid, cid) - out.append({ - "clientId": cid, - "clientName": nm, - "orderAmount": m["orderAmount"], - "productQuantity": m["productQuantity"], - }) - return out - - @staticmethod - def get_financial_statistics( - db: Session, - region_id: str = None, - start_date: str = None, - end_date: str = None - ) -> Dict[str, Any]: - """获取财务统计数据""" - order_query = db.query(BizOrder).filter(BizOrder.is_del == 0) - settlement_query = db.query(BizSettlement).filter(BizSettlement.is_del == 0) - refund_query = db.query(BizRefund).filter(BizRefund.is_del == 0) - - if region_id: - order_query = order_query.filter(BizOrder.region_id == region_id) - settlement_query = settlement_query.filter(BizSettlement.region_id == region_id) - - if start_date: - try: - start_dt = datetime.strptime(start_date, "%Y-%m-%d") - order_query = order_query.filter(BizOrder.create_at >= start_dt) - settlement_query = settlement_query.filter(BizSettlement.create_at >= start_dt) - refund_query = refund_query.filter(BizRefund.create_at >= start_dt) - except: - pass - - if end_date: - try: - end_dt = datetime.strptime(end_date, "%Y-%m-%d") - end_dt = end_dt.replace(hour=23, minute=59, second=59) - order_query = order_query.filter(BizOrder.create_at <= end_dt) - settlement_query = settlement_query.filter(BizSettlement.create_at <= end_dt) - refund_query = refund_query.filter(BizRefund.create_at <= end_dt) - except: - pass - - orders = order_query.all() - settlements = settlement_query.filter(BizSettlement.status == "confirmed").all() - refunds = refund_query.filter(BizRefund.status == "completed").all() - - total_revenue = sum(float(o.total_amount or 0) for o in orders) - total_received = sum(float(s.amount or 0) for s in settlements) - total_refund = sum(float(r.refund_amount or 0) for r in refunds) - net_revenue = total_received - total_refund - - return { - "totalRevenue": round(total_revenue, 2), - "totalReceived": round(total_received, 2), - "totalRefund": round(total_refund, 2), - "netRevenue": round(net_revenue, 2), - "orderCount": len(orders), - "settlementCount": len(settlements), - "refundCount": len(refunds) - } - - @staticmethod - def get_financial_trend( - db: Session, - region_id: str = None, - days: int = 7 - ) -> List[Dict[str, Any]]: - """获取财务趋势数据(按日统计)""" - result = [] - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - - for i in range(days - 1, -1, -1): - day_start = today - timedelta(days=i) - day_end = day_start + timedelta(days=1) - - settlement_query = db.query(BizSettlement).filter( - and_( - BizSettlement.is_del == 0, - BizSettlement.status == "confirmed", - BizSettlement.create_at >= day_start, - BizSettlement.create_at < day_end - ) - ) - - refund_query = db.query(BizRefund).filter( - and_( - BizRefund.is_del == 0, - BizRefund.status == "completed", - BizRefund.create_at >= day_start, - BizRefund.create_at < day_end - ) - ) - - if region_id: - settlement_query = settlement_query.filter(BizSettlement.region_id == region_id) - - settlements = settlement_query.all() - refunds = refund_query.all() - - received = sum(float(s.amount or 0) for s in settlements) - refund = sum(float(r.refund_amount or 0) for r in refunds) - - result.append({ - "date": day_start.strftime("%Y-%m-%d"), - "received": round(received, 2), - "refund": round(refund, 2), - "net": round(received - refund, 2) - }) - - return result - - @staticmethod - def get_delivery_statistics( - db: Session, - region_id: str = None, - start_date: str = None, - end_date: str = None - ) -> Dict[str, Any]: - """获取配送统计数据""" - query = db.query(BizDeliveryTask).filter(BizDeliveryTask.is_del == 0) - - if region_id: - query = query.filter(BizDeliveryTask.region_id == region_id) - - if start_date: - try: - start_dt = datetime.strptime(start_date, "%Y-%m-%d") - query = query.filter(BizDeliveryTask.create_at >= start_dt) - except: - pass - - if end_date: - try: - end_dt = datetime.strptime(end_date, "%Y-%m-%d") - end_dt = end_dt.replace(hour=23, minute=59, second=59) - query = query.filter(BizDeliveryTask.create_at <= end_dt) - except: - pass - - tasks = query.all() - - pending = sum(1 for t in tasks if t.status == "pending") - picked_up = sum(1 for t in tasks if t.status == "picked_up") - delivering = sum(1 for t in tasks if t.status == "delivering") - completed = sum(1 for t in tasks if t.status == "completed") - cancelled = sum(1 for t in tasks if t.status == "cancelled") - - return { - "totalTasks": len(tasks), - "pending": pending, - "pickedUp": picked_up, - "delivering": delivering, - "completed": completed, - "cancelled": cancelled - } - - @staticmethod - def get_product_statistics( - db: Session, - region_id: str = None, - start_date: str = None, - end_date: str = None, - limit: int = 10 - ) -> List[Dict[str, Any]]: - """获取商品销售排行""" - query = db.query( - BizOrderItem.product_id, - func.sum(BizOrderItem.quantity).label("total_quantity"), - func.sum(BizOrderItem.amount).label("total_amount") - ).join( - BizOrder, - and_( - BizOrder.id == BizOrderItem.order_id, - BizOrder.is_del == 0, - BizOrder.status.in_(["completed", "delivering", "pending_confirm"]) - ) - ).filter( - BizOrderItem.is_del == 0 - ) - - if region_id: - query = query.filter(BizOrder.region_id == region_id) - - if start_date: - try: - start_dt = datetime.strptime(start_date, "%Y-%m-%d") - query = query.filter(BizOrder.create_at >= start_dt) - except: - pass - - if end_date: - try: - end_dt = datetime.strptime(end_date, "%Y-%m-%d") - end_dt = end_dt.replace(hour=23, minute=59, second=59) - query = query.filter(BizOrder.create_at <= end_dt) - except: - pass - - results = query.group_by(BizOrderItem.product_id).order_by( - desc("total_amount") - ).limit(limit).all() - - product_stats = [] - for r in results: - product = db.query(BasProduct).filter( - and_(BasProduct.id == r.product_id, BasProduct.is_del == 0) - ).first() - - product_stats.append({ - "productId": r.product_id, - "productName": product.name if product else None, - "totalQuantity": float(r.total_quantity or 0), - "totalAmount": float(r.total_amount or 0) - }) - - return product_stats - - @staticmethod - def get_customer_statistics( - db: Session, - region_id: str = None, - start_date: str = None, - end_date: str = None, - limit: int = 10 - ) -> List[Dict[str, Any]]: - """获取客户订单排行""" - query = db.query( - BizOrder.client_id, - func.count(BizOrder.id).label("order_count"), - func.sum(BizOrder.total_amount).label("total_amount") - ).filter( - and_( - BizOrder.is_del == 0, - BizOrder.status.in_(["completed", "delivering", "pending_confirm", "pending_payment"]) - ) - ) - - if region_id: - query = query.filter(BizOrder.region_id == region_id) - - if start_date: - try: - start_dt = datetime.strptime(start_date, "%Y-%m-%d") - query = query.filter(BizOrder.create_at >= start_dt) - except: - pass - - if end_date: - try: - end_dt = datetime.strptime(end_date, "%Y-%m-%d") - end_dt = end_dt.replace(hour=23, minute=59, second=59) - query = query.filter(BizOrder.create_at <= end_dt) - except: - pass - - results = query.group_by(BizOrder.client_id).order_by( - desc("total_amount") - ).limit(limit).all() - - customer_stats = [] - for r in results: - customer = db.query(SysUser).filter( - and_(SysUser.id == r.client_id, SysUser.is_del == 0) - ).first() - - customer_stats.append({ - "customerId": r.client_id, - "customerName": customer.real_name if customer else None, - "customerPhone": customer.phone if customer else None, - "orderCount": r.order_count, - "totalAmount": float(r.total_amount or 0) - }) - - return customer_stats - - @staticmethod - def _dashboard_order_region_clause(region_id: str = None): - return [BizOrder.region_id == region_id] if region_id else [] - - @staticmethod - def get_dashboard_summary( - db: Session, - region_id: str = None, - include_inactive_regions: bool = False, - ) -> Dict[str, Any]: - """获取仪表盘汇总数据(运营工作台;含待办摘要、最新订单、预警数等,字段驼峰)""" - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - tomorrow_start = today + timedelta(days=1) - month_start = today.replace(day=1) - yesterday_start = today - timedelta(days=1) - region_clause = StatisticsCRUD._dashboard_order_region_clause(region_id) - region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True] - - order_query = ( - db.query(BizOrder) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter( - and_( - BizOrder.is_del == 0, - BizOrder.create_at >= month_start, - SysRegion.is_del == 0, - *region_active_clause, - *region_clause, - ) - ) - ) - month_orders = order_query.all() - today_orders = [ - o for o in month_orders if o.create_at and o.create_at >= today - ] - prev_day_orders = [ - o - for o in month_orders - if o.create_at and yesterday_start <= o.create_at < today - ] - - pending_delivery_q = ( - db.query(BizOrder) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter( - and_( - BizOrder.is_del == 0, - BizOrder.delivery_status == "pending_dispatch", - SysRegion.is_del == 0, - *region_active_clause, - *region_clause, - ) - ) - ) - pending_delivery_count = pending_delivery_q.count() - - def _delta_hint(cur_n: int, prev_n: int, unit: str) -> str: - d = cur_n - prev_n - if d > 0: - return f"较昨日 +{d}{unit}" - if d < 0: - return f"较昨日 {d}{unit}" - return "与昨日持平" - - def _amount_delta_hint(cur_amt: float, prev_amt: float) -> str: - d = round(cur_amt - prev_amt, 2) - if d > 0.0001: - return f"较昨日 +¥{d:,.2f}" - if d < -0.0001: - return f"较昨日 ¥{d:,.2f}" - return "与昨日持平" - - today_order_count = len(today_orders) - today_order_amount = round(sum(float(o.total_amount or 0) for o in today_orders), 2) - prev_count = len(prev_day_orders) - prev_amount = round(sum(float(o.total_amount or 0) for o in prev_day_orders), 2) - - # 未解决的配送时效预警(可按区域通过任务表关联) - warn_q = ( - db.query(BizDeliveryWarning) - .join( - BizDeliveryTask, - and_( - BizDeliveryTask.id == BizDeliveryWarning.task_id, - BizDeliveryTask.is_del == 0, - ), - ) - .join(SysRegion, BizDeliveryTask.region_id == SysRegion.id) - .filter( - and_( - BizDeliveryWarning.is_del == 0, - BizDeliveryWarning.is_resolved == 0, - SysRegion.is_del == 0, - *region_active_clause, - ) - ) - ) - if region_id: - warn_q = warn_q.filter(BizDeliveryTask.region_id == region_id) - delivery_warning_count = warn_q.count() - - # 今日新提交报价:状态为已提交,且提交时间(无则用创建时间)落在当日 0 点~次日 0 点 - quot_submitted_at = func.coalesce(BizQuotation.submit_time, BizQuotation.create_at) - sub_quot_q = ( - db.query(BizQuotation) - .join(SysRegion, BizQuotation.region_id == SysRegion.id) - .filter( - and_( - BizQuotation.is_del == 0, - BizQuotation.status == "submitted", - quot_submitted_at >= today, - quot_submitted_at < tomorrow_start, - SysRegion.is_del == 0, - *region_active_clause, - ) - ) - ) - if region_id: - sub_quot_q = sub_quot_q.filter(BizQuotation.region_id == region_id) - pending_quotation_count = sub_quot_q.count() - - sub_quot_preview = ( - db.query(BizQuotation, BizInquiry) - .join(BizInquiry, BizInquiry.id == BizQuotation.inquiry_id) - .join(SysRegion, BizQuotation.region_id == SysRegion.id) - .filter( - BizQuotation.is_del == 0, - BizQuotation.status == "submitted", - quot_submitted_at >= today, - quot_submitted_at < tomorrow_start, - BizInquiry.is_del == 0, - SysRegion.is_del == 0, - *region_active_clause, - *([BizQuotation.region_id == region_id] if region_id else []), - ) - .order_by(desc(BizQuotation.submit_time), desc(BizQuotation.create_at)) - .limit(2) - .all() - ) - quotation_lines = [] - for q, inq in sub_quot_preview: - title = (inq.title or "询价单")[:48] - quotation_lines.append(title) - - pd_preview = ( - db.query(BizOrder) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter( - and_( - BizOrder.is_del == 0, - BizOrder.delivery_status == "pending_dispatch", - SysRegion.is_del == 0, - *region_active_clause, - *region_clause, - ) - ) - .order_by(desc(BizOrder.create_at)) - .limit(2) - .all() - ) - delivery_lines = [o.order_no for o in pd_preview] - - cancel_preview = ( - db.query(BizOrder) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter( - and_( - BizOrder.is_del == 0, - BizOrder.cancel_status == "pending_review", - SysRegion.is_del == 0, - *region_active_clause, - *region_clause, - ) - ) - .order_by(desc(BizOrder.update_at)) - .limit(2) - .all() - ) - cancel_client_ids = list({o.client_id for o in cancel_preview if o.client_id}) - cancel_clients = {} - if cancel_client_ids: - for u in ( - db.query(SysUser) - .filter(and_(SysUser.is_del == 0, SysUser.id.in_(cancel_client_ids))) - .all() - ): - cancel_clients[u.id] = u.real_name or u.phone or u.id - cancel_lines = [] - for o in cancel_preview: - nm = cancel_clients.get(o.client_id, "") - cancel_lines.append(f"{o.order_no}" + (f" · {nm[:16]}" if nm else "")) - - settle_preview = ( - db.query(BizOrder) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter( - and_( - BizOrder.is_del == 0, - BizOrder.client_settlement_status == "pending", - BizOrder.pay_status == "paid", - BizOrder.order_status.in_(["pending_confirm", "completed"]), - SysRegion.is_del == 0, - *region_active_clause, - *region_clause, - ) - ) - .order_by(desc(BizOrder.update_at)) - .limit(2) - .all() - ) - settle_client_ids = list({o.client_id for o in settle_preview if o.client_id}) - settle_clients = {} - if settle_client_ids: - for u in ( - db.query(SysUser) - .filter(and_(SysUser.is_del == 0, SysUser.id.in_(settle_client_ids))) - .all() - ): - settle_clients[u.id] = u.real_name or u.phone or u.id - settle_lines = [] - for o in settle_preview: - nm = settle_clients.get(o.client_id, "") - settle_lines.append(f"{o.order_no}" + (f" · {nm[:16]}" if nm else "")) - - pending_settlement_count = ( - db.query(BizOrder) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter( - and_( - BizOrder.is_del == 0, - BizOrder.client_settlement_status == "pending", - BizOrder.pay_status == "paid", - BizOrder.order_status.in_(["pending_confirm", "completed"]), - SysRegion.is_del == 0, - *region_active_clause, - *region_clause, - ) - ) - .count() - ) - - pending_cancel_count = ( - db.query(BizOrder) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter( - and_( - BizOrder.is_del == 0, - BizOrder.cancel_status == "pending_review", - SysRegion.is_del == 0, - *region_active_clause, - *region_clause, - ) - ) - .count() - ) - - # 最新订单(工作台列表) - recent_rows = ( - db.query(BizOrder) - .join(SysRegion, BizOrder.region_id == SysRegion.id) - .filter(and_(BizOrder.is_del == 0, SysRegion.is_del == 0, *region_active_clause, *region_clause)) - .order_by(desc(BizOrder.create_at)) - .limit(8) - .all() - ) - rid_set = list({o.region_id for o in recent_rows if o.region_id}) - cid_set = list({o.client_id for o in recent_rows if o.client_id}) - region_name_map = {} - if rid_set: - for r in db.query(SysRegion).filter( - and_(SysRegion.is_del == 0, *region_active_clause, SysRegion.id.in_(rid_set)) - ).all(): - region_name_map[r.id] = r.name - client_name_map = {} - if cid_set: - for u in db.query(SysUser).filter(and_(SysUser.is_del == 0, SysUser.id.in_(cid_set))).all(): - client_name_map[u.id] = u.real_name or u.phone or "-" - - recent_orders = [] - for o in recent_rows: - order_status = getattr(o, "order_status", None) or "pending_payment" - recent_orders.append( - { - "id": o.id, - "orderNo": o.order_no, - "clientName": client_name_map.get(o.client_id, "-"), - "regionName": region_name_map.get(o.region_id, "-"), - "totalAmount": float(o.total_amount) if o.total_amount else 0, - "orderStatus": order_status, - "cancelStatus": getattr(o, "cancel_status", None) or "none", - "deliveryStatus": getattr(o, "delivery_status", None) or "pending_dispatch", - "createAt": str(o.create_at) if o.create_at else None, - } - ) - - todo_snapshots = [ - { - "type": "quotation", - "title": "今日新提交报价", - "count": pending_quotation_count, - "items": quotation_lines, - }, - { - "type": "delivery", - "title": "待分配配送", - "count": pending_delivery_count, - "items": delivery_lines, - }, - { - "type": "cancel", - "title": "取消待审核", - "count": pending_cancel_count, - "items": cancel_lines, - }, - { - "type": "settlement", - "title": "待客户结算", - "count": pending_settlement_count, - "items": settle_lines, - }, - ] - - # “客户总数/供应商总数”口径:统计用户表(sys_user)并按 region_id 区分 - # - region_id 为空:不过滤全局 - # - region_id 传入:按 SysUser.region_id 过滤 - customer_count = ( - db.query(SysUser) - .join(SysRegion, SysUser.region_id == SysRegion.id) - .filter( - and_( - SysUser.is_del == 0, - SysUser.role == "client", - SysRegion.is_del == 0, - *region_active_clause, - *([SysUser.region_id == region_id] if region_id else []), - ) - ) - .count() - ) - - supplier_count = ( - db.query(SysUser) - .join(SysRegion, SysUser.region_id == SysRegion.id) - .filter( - and_( - SysUser.is_del == 0, - SysUser.role == "supplier", - SysRegion.is_del == 0, - *region_active_clause, - *([SysUser.region_id == region_id] if region_id else []), - ) - ) - .count() - ) - - return { - "todayOrderCount": today_order_count, - "todayOrderAmount": today_order_amount, - "monthOrderCount": len(month_orders), - "monthOrderAmount": round(sum(float(o.total_amount or 0) for o in month_orders), 2), - "pendingDeliveryCount": pending_delivery_count, - "completedOrderCount": sum( - 1 for o in month_orders if (getattr(o, "order_status", None) or o.status) == "completed" - ), - "activePriceListCount": ( - db.query(BizPriceList) - .join(SysRegion, BizPriceList.region_id == SysRegion.id) - .filter( - and_( - BizPriceList.is_del == 0, - BizPriceList.status == "active", - SysRegion.is_del == 0, - *region_active_clause, - *([BizPriceList.region_id == region_id] if region_id else []), - ) - ) - .count() - ), - "customerCount": customer_count, - "supplierCount": supplier_count, - "todayOrderCountHint": _delta_hint(today_order_count, prev_count, " 单"), - "todayOrderAmountHint": _amount_delta_hint(today_order_amount, prev_amount), - "deliveryWarningCount": delivery_warning_count, - "recentOrders": recent_orders, - "todoSnapshots": todo_snapshots, - } - - -statistics_crud = StatisticsCRUD() diff --git a/backend/app/crud/tenant.py b/backend/app/crud/tenant.py new file mode 100644 index 00000000..a4004790 --- /dev/null +++ b/backend/app/crud/tenant.py @@ -0,0 +1,167 @@ +"""租户 CRUD(sys_tenant)""" +from __future__ import annotations + +import math +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple + +from sqlalchemy import and_, func, or_ +from sqlalchemy.orm import Session + +from app.core.config import settings +from app.core.utils import generate_uuid +from app.models.models import SysTenant + + +def _row_to_dict(t: SysTenant) -> Dict[str, Any]: + return { + "id": t.id, + "tenantNo": t.tenant_no, + "tenantName": t.tenant_name, + "contactName": t.contact_name or "", + "contactPhone": t.contact_phone or "", + "orderNum": t.order_num if t.order_num is not None else 0, + "remark": t.remark or "", + "createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "", + "updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "", + } + + +def list_tenants( + db: Session, + *, + page: int = 1, + page_size: int = 10, + keyword: Optional[str] = None, +) -> Tuple[List[Dict[str, Any]], int]: + def _filtered(): + q0 = db.query(SysTenant).filter(SysTenant.del_flag == 0) + kw = (keyword or "").strip() + if kw: + like = f"%{kw}%" + q0 = q0.filter(or_(SysTenant.tenant_no.like(like), SysTenant.tenant_name.like(like))) + return q0 + + total = _filtered().order_by(None).with_entities(func.count(SysTenant.id)).scalar() or 0 + page = max(1, page) + page_size = min(max(1, page_size), 100) + rows = ( + _filtered() + .order_by(SysTenant.order_num.asc(), SysTenant.create_time.desc()) + .offset((page - 1) * page_size) + .limit(page_size) + .all() + ) + return [_row_to_dict(x) for x in rows], int(total) + + +def get_tenant(db: Session, tenant_id: str) -> Optional[SysTenant]: + return ( + db.query(SysTenant) + .filter(and_(SysTenant.id == tenant_id, SysTenant.del_flag == 0)) + .first() + ) + + +def tenant_no_exists(db: Session, tenant_no: str, exclude_id: Optional[str] = None) -> bool: + q = db.query(SysTenant).filter(and_(SysTenant.tenant_no == tenant_no.strip(), SysTenant.del_flag == 0)) + if exclude_id: + q = q.filter(SysTenant.id != exclude_id) + return q.first() is not None + + +def create_tenant( + db: Session, + *, + tenant_no: str, + tenant_name: str, + contact_name: Optional[str] = None, + contact_phone: Optional[str] = None, + order_num: Optional[int] = None, + remark: Optional[str] = None, + user_id: Optional[str] = None, +) -> SysTenant: + tn = (tenant_no or "").strip() + if not tn: + raise ValueError("租户编号不能为空") + if tenant_no_exists(db, tn): + raise ValueError("租户编号已存在") + obj = SysTenant( + id=generate_uuid(), + tenant_no=tn, + tenant_name=(tenant_name or "").strip() or tn, + contact_name=(contact_name or "").strip() or None, + contact_phone=(contact_phone or "").strip() or None, + order_num=order_num if order_num is not None else 0, + remark=(remark or "").strip() or None, + create_by=user_id, + create_dept_id="", + create_time=datetime.now(), + update_by=user_id, + update_time=datetime.now(), + del_flag=0, + ) + db.add(obj) + db.commit() + db.refresh(obj) + return obj + + +def update_tenant( + db: Session, + tenant_id: str, + *, + tenant_no: Optional[str] = None, + tenant_name: Optional[str] = None, + contact_name: Optional[str] = None, + contact_phone: Optional[str] = None, + order_num: Optional[int] = None, + remark: Optional[str] = None, + user_id: Optional[str] = None, +) -> SysTenant: + obj = get_tenant(db, tenant_id) + if not obj: + raise ValueError("租户不存在") + if tenant_no is not None: + new_no = tenant_no.strip() + if not new_no: + raise ValueError("租户编号不能为空") + if obj.tenant_no == settings.DEFAULT_TENANT_NO and new_no != settings.DEFAULT_TENANT_NO: + raise ValueError("默认租户编号不可修改") + if new_no != obj.tenant_no and tenant_no_exists(db, new_no, exclude_id=tenant_id): + raise ValueError("租户编号已存在") + obj.tenant_no = new_no + if tenant_name is not None: + obj.tenant_name = (tenant_name or "").strip() or obj.tenant_name + if contact_name is not None: + obj.contact_name = (contact_name or "").strip() or None + if contact_phone is not None: + obj.contact_phone = (contact_phone or "").strip() or None + if order_num is not None: + obj.order_num = order_num + if remark is not None: + obj.remark = (remark or "").strip() or None + obj.update_by = user_id + obj.update_time = datetime.now() + db.add(obj) + db.commit() + db.refresh(obj) + return obj + + +def soft_delete_tenant(db: Session, tenant_id: str) -> None: + obj = get_tenant(db, tenant_id) + if not obj: + raise ValueError("租户不存在") + if obj.tenant_no == settings.DEFAULT_TENANT_NO: + raise ValueError("默认租户不可删除") + obj.del_flag = 1 + obj.update_time = datetime.now() + db.add(obj) + db.commit() + + +def total_pages(total: int, page_size: int) -> int: + if total <= 0: + return 0 + return int(math.ceil(total / float(page_size))) diff --git a/backend/app/crud/unit.py b/backend/app/crud/unit.py deleted file mode 100644 index 1dba9ceb..00000000 --- a/backend/app/crud/unit.py +++ /dev/null @@ -1,72 +0,0 @@ -"""商品单位CRUD""" -from typing import Optional, List -from sqlalchemy.orm import Session -from sqlalchemy import and_ - -from app.models.models import BasProductUnit, BasProductUnitHis -from app.crud.base import CRUDBase - - -class UnitCRUD(CRUDBase[BasProductUnit]): - """商品单位CRUD操作""" - - def get_by_code(self, db: Session, code: str) -> Optional[BasProductUnit]: - """根据编码获取单位""" - return db.query(BasProductUnit).filter( - and_(BasProductUnit.code == code, BasProductUnit.is_del == 0) - ).first() - - def get_active_all(self, db: Session) -> List[BasProductUnit]: - """获取所有启用的单位""" - return db.query(BasProductUnit).filter( - and_(BasProductUnit.is_del == 0, BasProductUnit.is_active == True) - ).order_by(BasProductUnit.sort_order.asc(), BasProductUnit.id.asc()).all() - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BasProductUnit]: - """更新单位并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "name": original.name, - "code": original.code, - "is_active": original.is_active, - "sort_order": original.sort_order, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BasProductUnitHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, id: str) -> bool: - """删除单位并记录历史""" - original = self.get(db, id) - if not original: - return False - - his_data = { - "id": original.id, - "name": original.name, - "code": original.code, - "is_active": original.is_active, - "sort_order": original.sort_order, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = BasProductUnitHis(**his_data) - db.add(his) - - return self.delete(db, id) - - -unit_crud = UnitCRUD(BasProductUnit) diff --git a/backend/app/crud/user.py b/backend/app/crud/user.py index 6d2bbcf9..702dcf3e 100644 --- a/backend/app/crud/user.py +++ b/backend/app/crud/user.py @@ -1,177 +1,33 @@ -"""用户CRUD""" -from typing import Optional, List, Tuple -from sqlalchemy.orm import Session +"""用户 CRUD(V0.3 sys_user)""" +from typing import Optional + from sqlalchemy import and_ +from sqlalchemy.orm import Session -from app.models.models import SysUser, SysUserHis +from app.core.config import settings +from app.models.models import SysUser from app.crud.base import CRUDBase -from app.core.security import get_password_hash -from app.core.utils import generate_random_password - -# 运营端「重置密码」专用明文:仅服务端使用,禁止由 HTTP 请求传入 -DEFAULT_ADMIN_RESET_PLAIN_PASSWORD = "111111" class UserCRUD(CRUDBase[SysUser]): - """用户CRUD操作""" + """用户 CRUD:按租户 + 用户账号查询""" - def get_by_username(self, db: Session, username: str) -> Optional[SysUser]: - """根据用户名获取用户""" - return db.query(SysUser).filter( - and_(SysUser.username == username, SysUser.is_del == 0) - ).first() - - def get_by_phone(self, db: Session, phone: str) -> Optional[SysUser]: - """根据手机号获取用户""" - return db.query(SysUser).filter( - and_(SysUser.phone == phone, SysUser.is_del == 0) - ).first() - - def get_by_wx_mini_openid(self, db: Session, openid: str) -> Optional[SysUser]: - """根据微信小程序 openid 获取用户""" - if not openid: + def get_by_user_no(self, db: Session, user_no: str, tenant_no: Optional[str] = None) -> Optional[SysUser]: + tenant = tenant_no or settings.DEFAULT_TENANT_NO + un = (user_no or "").strip() + if not un: return None - return db.query(SysUser).filter( - and_(SysUser.wx_mini_openid == openid, SysUser.is_del == 0) - ).first() - - def get_multi_by_role(self, db: Session, role: str, skip: int = 0, limit: int = 100) -> List[SysUser]: - """根据角色获取用户列表""" - return db.query(SysUser).filter( - and_(SysUser.role == role, SysUser.is_del == 0) - ).offset(skip).limit(limit).all() - - def get_multi_by_region(self, db: Session, region_id: str, skip: int = 0, limit: int = 100) -> List[SysUser]: - """根据区域获取用户列表""" - return db.query(SysUser).filter( - and_(SysUser.region_id == region_id, SysUser.is_del == 0) - ).offset(skip).limit(limit).all() - - def get_multi_with_filters( - self, - db: Session, - role: str = None, - roles: Optional[List[str]] = None, - region_id: str = None, - keyword: str = None, - is_active: bool = None, - skip: int = 0, - limit: int = 10 - ) -> tuple[List[SysUser], int]: - """多条件查询用户""" - query = db.query(SysUser).filter(SysUser.is_del == 0) - - if role: - query = query.filter(SysUser.role == role) - elif roles: - role_values = [str(x).strip() for x in roles if str(x).strip()] - if role_values: - query = query.filter(SysUser.role.in_(role_values)) - if region_id: - query = query.filter(SysUser.region_id == region_id) - if is_active is not None: - query = query.filter(SysUser.is_active == is_active) - if keyword: - query = query.filter( - (SysUser.username.like(f"%{keyword}%")) | - (SysUser.real_name.like(f"%{keyword}%")) | - (SysUser.phone.like(f"%{keyword}%")) + return ( + db.query(SysUser) + .filter( + and_( + SysUser.user_no == un, + SysUser.tenant_no == tenant, + SysUser.del_flag == 0, + ) ) - - total = query.count() - users = query.order_by(SysUser.create_at.desc()).offset(skip).limit(limit).all() - return users, total - - def create_with_password(self, db: Session, obj_in: dict, creator: dict = None) -> Tuple[SysUser, Optional[str]]: - """创建用户。 - - 若请求体包含非空 password(运营端表单),则使用该密码写入 password_hash; - 否则生成随机密码,并在返回值中给出明文 initialPassword(仅此种情况返回,供管理员告知用户)。 - """ - obj_in = dict(obj_in) - raw_pwd = obj_in.pop("password", None) - plain: Optional[str] = None - if isinstance(raw_pwd, str): - plain = raw_pwd.strip() - if plain: - if len(plain) < 6: - raise ValueError("密码至少6位") - obj_in["password_hash"] = get_password_hash(plain) - initial_reveal: Optional[str] = None - else: - initial_reveal = generate_random_password() - obj_in["password_hash"] = get_password_hash(initial_reveal) - obj_in["is_active"] = obj_in.get("is_active", True) - user = self.create(db, obj_in, creator) - return user, initial_reveal - - def reset_password_to_admin_default(self, db: Session, id: str) -> bool: - """将密码重置为服务端固定默认(DEFAULT_ADMIN_RESET_PLAIN_PASSWORD),不由请求传入。""" - user = self.get(db, id) - if not user: - return False - plain = DEFAULT_ADMIN_RESET_PLAIN_PASSWORD - if len(plain) < 6: - raise ValueError("默认重置密码配置无效:至少6位") - user.password_hash = get_password_hash(plain) - db.commit() - return True - - def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[SysUser]: - """更新用户并记录历史""" - original = self.get(db, id) - if not original: - return None - - his_data = { - "id": original.id, - "username": original.username, - "password_hash": original.password_hash, - "real_name": original.real_name, - "phone": original.phone, - "avatar_url": getattr(original, "avatar_url", None), - "wx_mini_openid": getattr(original, "wx_mini_openid", None), - "role": original.role, - "region_id": original.region_id, - "is_active": original.is_active, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = SysUserHis(**his_data) - db.add(his) - - return self.update(db, id, obj_in, updater) - - def soft_delete_with_history(self, db: Session, id: str) -> bool: - """删除用户并记录历史""" - original = self.get(db, id) - if not original: - return False - - his_data = { - "id": original.id, - "username": original.username, - "password_hash": original.password_hash, - "real_name": original.real_name, - "phone": original.phone, - "avatar_url": getattr(original, "avatar_url", None), - "wx_mini_openid": getattr(original, "wx_mini_openid", None), - "role": original.role, - "region_id": original.region_id, - "is_active": original.is_active, - "create_at": original.create_at, - "create_by": original.create_by, - "create_name": original.create_name, - "update_at": original.update_at, - "update_by": original.update_by, - } - his = SysUserHis(**his_data) - db.add(his) - - return self.delete(db, id) + .first() + ) user_crud = UserCRUD(SysUser) diff --git a/backend/app/main.py b/backend/app/main.py index b59f7b3b..cec49f25 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,28 +1,89 @@ -"""FastAPI 骨架入口:无业务路由、无数据库依赖,仅健康检查与文档。""" +"""FastAPI 应用入口(骨架工程)""" +from contextlib import asynccontextmanager import logging from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.core.config import settings +from app.core.database import SessionLocal +from app.core.api_request_log_middleware import ApiRequestLogMiddleware +from app.core.api_crypto_middleware import ApiCryptoMiddleware +from app.models import models # noqa: F401 +from app.api import public_router, menu_router, admin_router + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """应用生命周期管理""" + from app.core import login_crypto + + login_crypto.ensure_initialized() + + from app.tasks import init_scheduled_tasks + + try: + init_scheduled_tasks(SessionLocal) + except Exception as e: + logging.warning("定时任务调度器启动失败: %s", e) + + yield + + from app.tasks import shutdown_scheduler + + try: + shutdown_scheduler() + except Exception: + pass -logger = logging.getLogger("zkxw") app = FastAPI( title=settings.APP_NAME, - description="中科兴皖一体化门户后端骨架(业务代码已移除,可按需从版本库恢复)", + description="zkxw 管理端骨架后端(鉴权与菜单)", version="1.0.0", docs_url="/docs", redoc_url="/redoc", + lifespan=lifespan, ) -app.add_middleware( - CORSMiddleware, - allow_origins=settings.BACKEND_CORS_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], +_cors_kw: dict = { + "allow_origins": settings.BACKEND_CORS_ORIGINS, + "allow_credentials": True, + "allow_methods": ["*"], + "allow_headers": ["*"], +} +if settings.DEBUG: + _cors_kw["allow_origin_regex"] = r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$" +app.add_middleware(CORSMiddleware, **_cors_kw) +app.add_middleware(ApiRequestLogMiddleware) +app.add_middleware(ApiCryptoMiddleware) + +app.include_router(public_router, prefix=settings.API_V1_PREFIX) +app.include_router(menu_router, prefix=settings.API_V1_PREFIX) +app.include_router(admin_router, prefix=settings.API_V1_PREFIX) + +logger = logging.getLogger("sqxp") + + +def _mask_secret(value: str) -> str: + if not value: + return "(empty)" + if len(value) <= 6: + return "*" * len(value) + return f"{value[:3]}***{value[-3:]}" + + +_api_crypto_boot_msg = ( + "API_CRYPTO_BOOT " + f"enabled={settings.API_ENCRYPT_ENABLED} " + f"fail_open={settings.API_ENCRYPT_FAIL_OPEN} " + f"fixed_secret={_mask_secret(settings.API_ENCRYPT_FIXED_SECRET)} " + f"ts_skew_sec={settings.API_ENCRYPT_TS_SKEW_SEC} " + f"nonce_ttl_sec={settings.API_ENCRYPT_NONCE_TTL_SEC} " + f"exclude_count={len([x for x in (settings.API_ENCRYPT_EXCLUDE_PATHS or '').split(',') if x.strip()])}" ) +print(_api_crypto_boot_msg) +logger.info(_api_crypto_boot_msg) @app.get("/") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index d90e0d5e..4aedc915 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,95 +1,30 @@ """数据模型模块""" from app.models.base import BaseModel, SoftDeleteModel from app.models.models import ( - SysUser, SysUserHis, - SysRegion, SysRegionHis, - SysApiRequestLog, SysPasswordHistory, - SysApiRequestLogHis, SysPasswordHistoryHis, - BasProductCategory, BasProductCategoryHis, - BasProductUnit, BasProductUnitHis, - BasProduct, BasProductHis, - BasChinaRegion, - BasAddress, BasAddressHis, - BizInquiry, BizInquiryItem, - BizQuotation, BizQuotationItem, - BizPriceList, BizPriceListItem, - BizOrder, BizOrderItem, BizOrderStatusLog, BizOrderAdjust, - BizDeliveryTask, BizDeliveryTaskItem, - BizSettlement, BizRefund, BizWechatPay, BizAttachment, - OrdClientSettlement, OrdClientSettlementHis, OrdClientSettlementItem, OrdClientSettlementItemHis, - OrdSupplierSettlement, OrdSupplierSettlementHis, OrdSupplierSettlementItem, OrdSupplierSettlementItemHis, - BizInvoice, BizSystemConfig, BizDelivererReview, - BizDelivererReviewHis, BizDeliveryWarning, BizDeliveryWarningHis, BizOrderStatusLogHis, - UserRole + SysUser, + SysRole, + SysUserRole, + SysRoleMenu, + SysMenu, + SysApiRequestLog, + SysApiRequestLogHis, + UserRole, ) -from app.models.product_banner import BasProductBanner, BasProductBannerHis -from app.models.product_detail_media import BasProductDetailMedia, BasProductDetailMediaHis -from app.models.product_parameter import BasProductParameter, BasProductParameterHis -from app.models.category_parameter import BasCategoryParameter, BasCategoryParameterHis -from app.models.product_parameter_value import BasProductParameterValue, BasProductParameterValueHis -from app.models.product_review import BizProductReview, BizProductReviewHis -from app.models.product_review_media import BizProductReviewMedia, BizProductReviewMediaHis -# 兼容旧的模型名称别名 -Supplier = BizDelivererReview -Client = SysUser # 客户复用SysUser,通过role字段区分 -Deliver = SysUser # 配送员复用SysUser,通过role字段区分 -Inquiry = BizInquiry -Quotation = BizQuotation -QuotationItem = BizQuotationItem -PriceList = BizPriceList -PriceListItem = BizPriceListItem -Order = BizOrder -OrderItem = BizOrderItem -OrderStatus = BizOrderStatusLog -DeliveryTask = BizDeliveryTask -DeliveryTaskItem = BizDeliveryTaskItem -Product = BasProduct -ProductUnit = BasProductUnit -Region = SysRegion User = SysUser ApiRequestLog = SysApiRequestLog __all__ = [ "BaseModel", "SoftDeleteModel", - # 系统模块 - "SysUser", "SysUserHis", - "SysRegion", "SysRegionHis", - "SysApiRequestLog", "SysPasswordHistory", - "SysApiRequestLogHis", "SysPasswordHistoryHis", - # 基础模块 - "BasProductCategory", "BasProductCategoryHis", - "BasProductUnit", "BasProductUnitHis", - "BasProduct", "BasProductHis", - "BasChinaRegion", - "BasAddress", "BasAddressHis", - # 商品多媒体 - "BasProductBanner", "BasProductBannerHis", - "BasProductDetailMedia", "BasProductDetailMediaHis", - # 商品参数 - "BasProductParameter", "BasProductParameterHis", - "BasCategoryParameter", "BasCategoryParameterHis", - "BasProductParameterValue", "BasProductParameterValueHis", - # 售后评价 - "BizProductReview", "BizProductReviewHis", - "BizProductReviewMedia", "BizProductReviewMediaHis", - # 业务模块 - "BizInquiry", "BizInquiryItem", - "BizQuotation", "BizQuotationItem", - "BizPriceList", "BizPriceListItem", - "BizOrder", "BizOrderItem", "BizOrderStatusLog", "BizOrderAdjust", - "BizDeliveryTask", "BizDeliveryTaskItem", - "BizSettlement", "BizRefund", "BizWechatPay", "BizAttachment", - "OrdClientSettlement", "OrdClientSettlementHis", "OrdClientSettlementItem", "OrdClientSettlementItemHis", - "OrdSupplierSettlement", "OrdSupplierSettlementHis", "OrdSupplierSettlementItem", "OrdSupplierSettlementItemHis", - "BizInvoice", "BizSystemConfig", "BizDelivererReview", - "BizDelivererReviewHis", "BizDeliveryWarning", "BizDeliveryWarningHis", "BizOrderStatusLogHis", + "SysUser", + "SysRole", + "SysUserRole", + "SysRoleMenu", + "SysMenu", + "SysApiRequestLog", + "SysApiRequestLogHis", "UserRole", - # 兼容别名 - "Supplier", "Inquiry", "Quotation", "QuotationItem", - "PriceList", "PriceListItem", "Order", "OrderItem", - "OrderStatus", "DeliveryTask", "DeliveryTaskItem", - "Product", "ProductUnit", "Region", "User", + "User", "ApiRequestLog", ] diff --git a/backend/app/models/category_parameter.py b/backend/app/models/category_parameter.py deleted file mode 100644 index e5d85a94..00000000 --- a/backend/app/models/category_parameter.py +++ /dev/null @@ -1,42 +0,0 @@ -"""分类参数关联模型""" -from datetime import datetime - -from sqlalchemy import Column, String, DateTime, Integer - -from app.core.database import Base - - -class BasCategoryParameter(Base): - """商品分类-参数关联表""" - __tablename__ = "bas_category_parameter" - - id = Column(String(32), primary_key=True, comment="主键ID") - category_id = Column(String(32), nullable=False, comment="分类ID") - parameter_id = Column(String(32), nullable=False, comment="参数ID") - is_required = Column(Integer, default=0, comment="该分类下是否必填:0-否, 1-是") - default_value = Column(String(500), nullable=True, comment="默认值") - sort_order = Column(Integer, default=0, comment="排序") - is_del = Column(Integer, default=0, comment="是否删除:0-否, 1-是") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BasCategoryParameterHis(Base): - """商品分类-参数关联表历史表""" - __tablename__ = "bas_category_parameter_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - category_id = Column(String(32), nullable=True, comment="分类ID") - parameter_id = Column(String(32), nullable=True, comment="参数ID") - is_required = Column(Integer, default=0, comment="是否必填") - default_value = Column(String(500), nullable=True, comment="默认值") - sort_order = Column(Integer, default=0, comment="排序") - create_at = Column(DateTime, nullable=True, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 580a36d4..28ab5119 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -1,137 +1,165 @@ -"""数据模型""" +"""数据模型(与 docs/sql/zkxw-requirements-v0.3-schema.sql 对齐的核心系统表)""" from datetime import datetime +import enum -from sqlalchemy import Column, String, DateTime, Integer, Boolean, Text, Numeric, PrimaryKeyConstraint -from sqlalchemy.orm import relationship +from sqlalchemy import Column, String, DateTime, Integer, Text, CHAR, SmallInteger from app.core.database import Base -import enum class UserRole(str, enum.Enum): - """用户角色""" - ADMIN = "admin" # 运营人员 - SUPPLIER = "supplier" # 供应商 - CLIENT = "client" # 客户 - DELIVER = "deliver" # 配送人员 + """Web 管理端约定角色(由 sys_role.role_name 映射而来,非数据库枚举)""" + + ADMIN = "admin" + SUPER_ADMIN = "super_admin" -# ========================================= -# 系统模块 -# ========================================= +# ============================================================================= +# V0.3 用户 / 角色 / 菜单(2.1.4 ~ 2.1.6) +# ============================================================================= + class SysUser(Base): - """用户表""" + """用户表 sys_user(V0.3)""" + __tablename__ = "sys_user" - id = Column(String(32), primary_key=True, comment="主键ID") - username = Column(String(50), unique=True, nullable=False, comment="用户名") - password_hash = Column(String(255), nullable=False, comment="密码哈希") - real_name = Column(String(100), nullable=True, comment="真实姓名") - phone = Column(String(20), nullable=True, comment="联系电话") - avatar_url = Column(String(512), nullable=True, comment="头像URL") - wx_mini_openid = Column(String(64), nullable=True, unique=True, comment="微信小程序openid") - role = Column(String(20), nullable=False, comment="角色") - region_id = Column(String(32), nullable=True, comment="所属区域ID") - is_active = Column(Boolean, default=True, comment="是否激活") - is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") + id = Column(String(64), primary_key=True, comment="主键ID") + dept_id = Column(String(64), nullable=True, comment="部门ID") + user_no = Column(String(30), nullable=False, comment="用户账号") + nick_name = Column(String(30), nullable=False, comment="用户名") + email = Column(String(50), nullable=False, default="", comment="用户邮箱") + phone_no = Column(String(11), nullable=False, default="", comment="手机号码") + sex = Column(CHAR(1), nullable=True, default="0", comment="用户性别") + avatar = Column(String(512), nullable=True, comment="头像") + password = Column("password", String(100), nullable=False, default="", comment="密码") + status = Column(CHAR(1), nullable=False, default="1", comment="帐号状态 1正常 0停用") + tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号") + order_num = Column(Integer, nullable=True, comment="排序号") + remark = Column(String(512), nullable=True, comment="备注") + create_by = Column(String(64), nullable=True, comment="创建人id") + create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID") + create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间") + update_by = Column(String(64), nullable=True, comment="更新人id") + update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间") + del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记 0正常 1已删除") -class SysUserHis(Base): - """用户历史表""" - __tablename__ = "sys_user_his" +class SysRole(Base): + """角色表 sys_role(V0.3)""" - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - username = Column(String(50), nullable=False, comment="用户名") - password_hash = Column(String(255), nullable=False, comment="密码哈希") - real_name = Column(String(100), nullable=True, comment="真实姓名") - phone = Column(String(20), nullable=True, comment="联系电话") - avatar_url = Column(String(512), nullable=True, comment="头像URL") - wx_mini_openid = Column(String(64), nullable=True, comment="微信小程序openid") - role = Column(String(20), nullable=False, comment="角色") - region_id = Column(String(32), nullable=True, comment="所属区域ID") - is_active = Column(Boolean, default=True, comment="是否激活") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") + __tablename__ = "sys_role" + + id = Column(String(64), primary_key=True, comment="主键ID") + role_name = Column(String(30), nullable=False, comment="角色名称") + data_scope = Column(CHAR(1), nullable=True, default="1", comment="数据权限") + status = Column(CHAR(1), nullable=False, default="1", comment="角色状态") + tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号") + order_num = Column(Integer, nullable=True, comment="排序号") + remark = Column(String(512), nullable=True, comment="备注") + create_by = Column(String(64), nullable=True, comment="创建人id") + create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID") + create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间") + update_by = Column(String(64), nullable=True, comment="更新人id") + update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间") + del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记") -class SysRegion(Base): - """区域表""" - __tablename__ = "sys_region" +class SysUserRole(Base): + """用户-角色关联 sys_user_role(V0.3)""" - id = Column(String(32), primary_key=True, comment="主键ID") - name = Column(String(100), nullable=False, comment="区域名称") - code = Column(String(50), unique=True, nullable=False, comment="区域编码") - description = Column(String(500), nullable=True, comment="区域描述") - sort_order = Column(Integer, default=0, comment="排序") - expected_delivery_minutes = Column(Integer, nullable=True, comment="预计配送时长(分钟)") - delivery_warn_advance_minutes = Column( - Integer, nullable=True, comment="计划到货前未送达时预警提前时间(分钟)" - ) - is_active = Column(Boolean, default=True, comment="是否启用") - is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") + __tablename__ = "sys_user_role" + + id = Column(String(64), primary_key=True, comment="主键ID") + user_id = Column(String(64), nullable=False, comment="用户ID") + role_id = Column(String(64), nullable=False, comment="角色ID") + tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号") + order_num = Column(Integer, nullable=True, comment="排序号") + remark = Column(String(512), nullable=True, comment="备注") + create_by = Column(String(64), nullable=True, comment="创建人id") + create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID") + create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间") + update_by = Column(String(64), nullable=True, comment="更新人id") + update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间") + del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记") -class SysRegionHis(Base): - """区域历史表""" - __tablename__ = "sys_region_his" +class SysRoleMenu(Base): + """角色-菜单关联 sys_role_menu(V0.3)""" - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - name = Column(String(100), nullable=False, comment="区域名称") - code = Column(String(50), nullable=False, comment="区域编码") - description = Column(String(500), nullable=True, comment="区域描述") - sort_order = Column(Integer, default=0, comment="排序") - expected_delivery_minutes = Column(Integer, nullable=True, comment="预计配送时长(分钟)") - delivery_warn_advance_minutes = Column( - Integer, nullable=True, comment="计划到货前未送达时预警提前时间(分钟)" - ) - is_active = Column(Boolean, default=True, comment="是否启用") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") + __tablename__ = "sys_role_menu" + + id = Column(String(64), primary_key=True, comment="主键ID") + role_id = Column(String(64), nullable=False, comment="角色ID") + menu_id = Column(String(64), nullable=False, comment="菜单ID") + tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号") + order_num = Column(Integer, nullable=True, comment="排序号") + remark = Column(String(512), nullable=True, comment="备注") + create_by = Column(String(64), nullable=True, comment="创建人id") + create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID") + create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间") + update_by = Column(String(64), nullable=True, comment="更新人id") + update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间") + del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记") -class BasChinaRegion(Base): - """中国行政区划(GB/T 2260),见 docs/sql/bas_china_region.sql""" +class SysMenu(Base): + """菜单表 sys_menu(V0.3)""" - __tablename__ = "bas_china_region" + __tablename__ = "sys_menu" - id = Column(String(32), primary_key=True, comment="主键,与 region_code 一致") - parent_id = Column(String(32), nullable=True, comment="父级 region_code,省级为 NULL") - region_code = Column(String(6), nullable=False, comment="六位行政区划码") - region_name = Column(String(100), nullable=False, comment="区划名称") - level = Column(Integer, nullable=False, comment="层级:1省 2市 3区县") - sort_order = Column(Integer, nullable=False, default=0, comment="排序") - is_del = Column(Integer, default=0, nullable=False, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") + id = Column(String(64), primary_key=True, comment="主键ID") + menu_name = Column(String(50), nullable=False, comment="菜单名称") + parent_id = Column(String(64), nullable=False, default="0", comment="父菜单ID") + menu_type = Column(CHAR(1), nullable=True, default="M", comment="类型 M目录 C菜单 F按钮") + path = Column(String(200), nullable=False, default="", comment="路由路径") + component = Column(String(255), nullable=True, comment="组件路径") + perms = Column(String(100), nullable=True, comment="权限标识") + is_frame = Column(Integer, nullable=True, default=1, comment="是否外链") + visible = Column(CHAR(1), nullable=True, default="1", comment="显示状态") + status = Column(CHAR(1), nullable=False, default="1", comment="菜单状态") + tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号") + order_num = Column(Integer, nullable=True, comment="排序号") + remark = Column(String(512), nullable=True, comment="备注") + create_by = Column(String(64), nullable=True, comment="创建人id") + create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID") + create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间") + update_by = Column(String(64), nullable=True, comment="更新人id") + update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间") + del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记") + + +class SysTenant(Base): + """租户表 sys_tenant(V0.3 2.1.1)""" + + __tablename__ = "sys_tenant" + + id = Column(String(64), primary_key=True, comment="主键ID") + tenant_no = Column(String(64), nullable=False, comment="租户编号(业务编码)") + tenant_name = Column(String(200), nullable=False, comment="租户名称") + contact_name = Column(String(100), nullable=True, comment="联系人姓名") + contact_phone = Column(String(32), nullable=True, comment="联系电话") + order_num = Column(Integer, nullable=True, comment="排序号") + remark = Column(String(512), nullable=True, comment="备注") + create_by = Column(String(64), nullable=True, comment="创建人id") + create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID") + create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间") + update_by = Column(String(64), nullable=True, comment="更新人id") + update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间") + del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除") + + +# ============================================================================= +# 可选:API 请求日志(不在 V0.3 DDL 内;无表时中间件写入会失败并记录 warning) +# ============================================================================= class SysApiRequestLog(Base): - """API 请求日志(统一记录请求/响应出入参)""" + """API 请求日志""" + __tablename__ = "sys_api_request_log" - id = Column(String(32), primary_key=True, comment="主键ID") + id = Column(String(64), primary_key=True, comment="主键ID") http_method = Column(String(10), nullable=False, comment="HTTP 方法") path = Column(String(500), nullable=False, comment="请求路径") query_string = Column(String(2000), nullable=True, comment="Query 字符串") @@ -139,9 +167,9 @@ class SysApiRequestLog(Base): response_body = Column(Text, nullable=True, comment="响应体(截断)") status_code = Column(Integer, nullable=False, comment="HTTP 状态码") duration_ms = Column(Integer, nullable=False, comment="耗时(ms)") - user_id = Column(String(32), nullable=True, comment="用户ID(JWT 解析)") + user_id = Column(String(64), nullable=True, comment="用户ID(JWT 解析)") username = Column(String(100), nullable=True, comment="用户名") - role = Column(String(20), nullable=True, comment="角色") + role = Column(String(64), nullable=True, comment="角色") ip_address = Column(String(50), nullable=True, comment="IP地址") user_agent = Column(String(500), nullable=True, comment="User-Agent") is_del = Column(Integer, default=0, comment="是否删除") @@ -152,1257 +180,12 @@ class SysApiRequestLog(Base): update_by = Column(String(32), nullable=True, comment="修改人ID") -class SysMenu(Base): - """菜单表""" - __tablename__ = "sys_menu" - - id = Column(String(32), primary_key=True, comment="主键ID") - name = Column(String(100), nullable=False, comment="菜单名称") - code = Column(String(100), nullable=False, comment="菜单编码(唯一标识)") - path = Column(String(500), nullable=False, comment="路由路径") - component = Column(String(500), nullable=True, comment="组件路径") - icon = Column(String(100), nullable=True, comment="菜单图标") - parent_id = Column(String(32), nullable=True, comment="父菜单ID") - sort_order = Column(Integer, default=0, comment="排序(数字越小越靠前)") - redirect = Column(String(500), nullable=True, comment="重定向路径") - menu_type = Column(String(20), default="menu", comment="菜单类型: menu-菜单, button-按钮") - is_visible = Column(Integer, default=1, comment="是否显示: 0-隐藏, 1-显示") - is_cache = Column(Integer, default=0, comment="是否缓存: 0-不缓存, 1-缓存") - is_active = Column(Integer, default=1, comment="是否启用: 0-禁用, 1-启用") - is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是") - remark = Column(String(500), nullable=True, comment="备注") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class SysUserRegionScope(Base): - """用户区域权限表""" - __tablename__ = "sys_user_region_scope" - - id = Column(String(32), primary_key=True, comment="主键ID") - user_id = Column(String(32), nullable=False, comment="用户ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class SysUserRegionScopeHis(Base): - """用户区域权限历史表""" - __tablename__ = "sys_user_region_scope_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - user_id = Column(String(32), nullable=False, comment="用户ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class SysUserMenuScope(Base): - """用户菜单权限表(按菜单 code 授权)""" - __tablename__ = "sys_user_menu_scope" - - id = Column(String(32), primary_key=True, comment="主键ID") - user_id = Column(String(32), nullable=False, comment="用户ID") - menu_code = Column(String(100), nullable=False, comment="菜单编码") - is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class SysUserMenuScopeHis(Base): - """用户菜单权限历史表""" - __tablename__ = "sys_user_menu_scope_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - user_id = Column(String(32), nullable=False, comment="用户ID") - menu_code = Column(String(100), nullable=False, comment="菜单编码") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class SysPasswordHistory(Base): - """密码修改历史表""" - __tablename__ = "sys_password_history" - - id = Column(String(32), primary_key=True, comment="主键ID") - user_id = Column(String(32), nullable=False, comment="用户ID") - operation_type = Column(String(20), nullable=False, comment="操作类型") - operator_id = Column(String(32), nullable=True, comment="操作人ID") - operator_name = Column(String(100), nullable=True, comment="操作人姓名") - ip_address = Column(String(50), nullable=True, comment="IP地址") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -# ========================================= -# 基础模块 -# ========================================= - -class BasProductCategory(Base): - """商品分类表""" - __tablename__ = "bas_product_category" - - id = Column(String(32), primary_key=True, comment="主键ID") - name = Column(String(100), nullable=False, comment="分类名称") - image_url = Column(String(500), nullable=True, comment="图片URL") - parent_id = Column(String(32), nullable=True, comment="父分类ID") - sort_order = Column(Integer, default=0, comment="排序") - price_coefficient = Column( - Numeric(6, 2), default=0, nullable=False, comment="价格系数(百分比点数,5表示5%)" - ) - is_active = Column(Boolean, default=True, comment="是否启用") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BasProductCategoryHis(Base): - """商品分类历史表""" - __tablename__ = "bas_product_category_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - name = Column(String(100), nullable=False, comment="分类名称") - image_url = Column(String(500), nullable=True, comment="图片URL") - parent_id = Column(String(32), nullable=True, comment="父分类ID") - sort_order = Column(Integer, default=0, comment="排序") - price_coefficient = Column( - Numeric(6, 2), default=0, nullable=False, comment="价格系数(百分比点数,5表示5%)" - ) - is_active = Column(Boolean, default=True, comment="是否启用") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BasProductUnit(Base): - """商品单位码表""" - __tablename__ = "bas_product_unit" - - id = Column(String(32), primary_key=True, comment="主键ID") - name = Column(String(50), nullable=False, comment="单位名称") - code = Column(String(20), unique=True, nullable=False, comment="单位编码") - is_active = Column(Boolean, default=True, comment="是否启用") - sort_order = Column(Integer, default=0, comment="排序字段,数值越小越靠前") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BasProductUnitHis(Base): - """商品单位历史表""" - __tablename__ = "bas_product_unit_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - name = Column(String(50), nullable=False, comment="单位名称") - code = Column(String(20), nullable=False, comment="单位编码") - is_active = Column(Boolean, default=True, comment="是否启用") - sort_order = Column(Integer, default=0, comment="排序字段,数值越小越靠前") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BasProduct(Base): - """商品表""" - __tablename__ = "bas_product" - - id = Column(String(32), primary_key=True, comment="主键ID") - name = Column(String(200), nullable=False, comment="商品名称") - sku_code = Column(String(50), nullable=True, unique=True, comment="商品SKU编码") - category_id = Column(String(32), nullable=True, comment="分类ID") - unit_id = Column(String(32), nullable=True, comment="单位ID") - measure_value = Column(Numeric(12, 3), nullable=True, comment="计量数量(与单位组合表示售卖规格,如500克、1个)") - description = Column(String(1000), nullable=True, comment="商品描述") - detail_content = Column(Text, nullable=True, comment="商品详情富文本内容") - image_url = Column(String(500), nullable=True, comment="图片URL") - price_coefficient = Column( - Numeric(6, 2), default=0, nullable=False, comment="调价系数(百分比点数,5表示5%)" - ) - is_active = Column(Boolean, default=True, comment="是否启用") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - category = relationship( - "BasProductCategory", - foreign_keys=[category_id], - primaryjoin="BasProduct.category_id==BasProductCategory.id", - lazy="select", - ) - unit = relationship( - "BasProductUnit", - foreign_keys=[unit_id], - primaryjoin="BasProduct.unit_id==BasProductUnit.id", - lazy="select", - ) - - -class BasProductHis(Base): - """商品历史表""" - __tablename__ = "bas_product_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - name = Column(String(200), nullable=False, comment="商品名称") - sku_code = Column(String(50), nullable=True, comment="商品SKU编码") - category_id = Column(String(32), nullable=True, comment="分类ID") - unit_id = Column(String(32), nullable=True, comment="单位ID") - measure_value = Column(Numeric(12, 3), nullable=True, comment="计量数量(与单位组合表示售卖规格,如500克、1个)") - description = Column(String(1000), nullable=True, comment="商品描述") - detail_content = Column(Text, nullable=True, comment="商品详情富文本内容") - image_url = Column(String(500), nullable=True, comment="图片URL") - price_coefficient = Column( - Numeric(6, 2), default=0, nullable=False, comment="调价系数(百分比点数,5表示5%)" - ) - is_active = Column(Boolean, default=True, comment="是否启用") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BasAddress(Base): - """地址表""" - __tablename__ = "bas_address" - - id = Column(String(32), primary_key=True, comment="主键ID") - user_id = Column(String(32), nullable=False, comment="用户ID") - address_type = Column(String(20), nullable=False, comment="地址类型") - contact_person = Column(String(100), nullable=False, comment="联系人") - contact_phone = Column(String(20), nullable=False, comment="联系电话") - province = Column(String(50), nullable=True, comment="省份") - city = Column(String(50), nullable=True, comment="城市") - district = Column(String(50), nullable=True, comment="区县") - detail_address = Column(String(500), nullable=False, comment="详细地址") - is_default = Column(Boolean, default=False, comment="是否默认地址") - latitude = Column(String(20), nullable=True, comment="纬度") - longitude = Column(String(20), nullable=True, comment="经度") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BasAddressHis(Base): - """地址历史表""" - __tablename__ = "bas_address_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - user_id = Column(String(32), nullable=False, comment="用户ID") - address_type = Column(String(20), nullable=False, comment="地址类型") - contact_person = Column(String(100), nullable=False, comment="联系人") - contact_phone = Column(String(20), nullable=False, comment="联系电话") - province = Column(String(50), nullable=True, comment="省份") - city = Column(String(50), nullable=True, comment="城市") - district = Column(String(50), nullable=True, comment="区县") - detail_address = Column(String(500), nullable=False, comment="详细地址") - is_default = Column(Boolean, default=False, comment="是否默认地址") - latitude = Column(String(20), nullable=True, comment="纬度") - longitude = Column(String(20), nullable=True, comment="经度") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -# ========================================= -# 业务模块 -# ========================================= - -class BizInquiry(Base): - """询价单表""" - __tablename__ = "biz_inquiry" - - id = Column(String(32), primary_key=True, comment="主键ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - title = Column(String(200), nullable=False, comment="询价单标题") - status = Column(String(20), default="draft", comment="状态") - publish_time = Column(DateTime, nullable=True, comment="发布时间") - close_time = Column(DateTime, nullable=True, comment="关闭时间") - remark = Column(String(1000), nullable=True, comment="备注") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizInquiryItem(Base): - """询价单项表""" - __tablename__ = "biz_inquiry_item" - - id = Column(String(32), primary_key=True, comment="主键ID") - inquiry_id = Column(String(32), nullable=False, comment="询价单ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizInquiryHis(Base): - """询价单历史表""" - __tablename__ = "biz_inquiry_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - title = Column(String(200), nullable=False, comment="询价单标题") - status = Column(String(20), default="draft", comment="状态") - publish_time = Column(DateTime, nullable=True, comment="发布时间") - close_time = Column(DateTime, nullable=True, comment="关闭时间") - remark = Column(String(1000), nullable=True, comment="备注") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizInquiryItemHis(Base): - """询价单项历史表""" - __tablename__ = "biz_inquiry_item_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - inquiry_id = Column(String(32), nullable=False, comment="询价单ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizQuotation(Base): - """报价单表""" - __tablename__ = "biz_quotation" - - id = Column(String(32), primary_key=True, comment="主键ID") - inquiry_id = Column(String(32), nullable=False, comment="询价单ID") - supplier_id = Column(String(32), nullable=False, comment="供应商ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - # 报价无需审核:仅使用 submitted / selected - status = Column(String(20), default="submitted", comment="状态") - total_amount = Column(Numeric(12, 2), default=0, comment="报价总金额") - submit_time = Column(DateTime, nullable=True, comment="提交时间") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizQuotationItem(Base): - """报价单项表""" - __tablename__ = "biz_quotation_item" - - id = Column(String(32), primary_key=True, comment="主键ID") - quotation_id = Column(String(32), nullable=False, comment="报价单ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - unit_id = Column(String(32), nullable=False, comment="单位ID") - price = Column(Numeric(10, 2), nullable=False, comment="单价") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizQuotationHis(Base): - """报价单历史表""" - __tablename__ = "biz_quotation_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - inquiry_id = Column(String(32), nullable=False, comment="询价单ID") - supplier_id = Column(String(32), nullable=False, comment="供应商ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - status = Column(String(20), default="submitted", comment="状态") - total_amount = Column(Numeric(12, 2), default=0, comment="报价总金额") - submit_time = Column(DateTime, nullable=True, comment="提交时间") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizQuotationItemHis(Base): - """报价单项历史表""" - __tablename__ = "biz_quotation_item_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - quotation_id = Column(String(32), nullable=False, comment="报价单ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - unit_id = Column(String(32), nullable=False, comment="单位ID") - price = Column(Numeric(10, 2), nullable=False, comment="单价") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizPriceList(Base): - """价格清单表""" - __tablename__ = "biz_price_list" - - id = Column(String(32), primary_key=True, comment="主键ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - supplier_id = Column(String(32), nullable=False, comment="选用供应商ID") - inquiry_id = Column(String(32), nullable=True, comment="来源询价单ID") - quotation_id = Column(String(32), nullable=True, comment="来源报价单ID") - source_type = Column(String(20), nullable=True, comment="来源类型") - version = Column(Integer, default=1, comment="版本号") - status = Column(String(20), default="draft", comment="状态") - effective_time = Column(DateTime, nullable=True, comment="生效时间") - published_by = Column(String(32), nullable=True, comment="发布人ID") - published_time = Column(DateTime, nullable=True, comment="发布时间") - remark = Column(String(500), nullable=True, comment="备注") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizPriceListHis(Base): - """价格清单历史表""" - __tablename__ = "biz_price_list_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - supplier_id = Column(String(32), nullable=False, comment="选用供应商ID") - inquiry_id = Column(String(32), nullable=True, comment="来源询价单ID") - quotation_id = Column(String(32), nullable=True, comment="来源报价单ID") - source_type = Column(String(20), nullable=True, comment="来源类型") - version = Column(Integer, default=1, comment="版本号") - status = Column(String(20), default="draft", comment="状态") - effective_time = Column(DateTime, nullable=True, comment="生效时间") - published_by = Column(String(32), nullable=True, comment="发布人ID") - published_time = Column(DateTime, nullable=True, comment="发布时间") - remark = Column(String(500), nullable=True, comment="备注") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizPriceListItem(Base): - """价格清单项表""" - __tablename__ = "biz_price_list_item" - - id = Column(String(32), primary_key=True, comment="主键ID") - price_list_id = Column(String(32), nullable=False, comment="价格清单ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - unit_id = Column(String(32), nullable=False, comment="单位ID") - price = Column(Numeric(10, 2), nullable=False, comment="单价") - pricing_coefficient = Column(Numeric(8, 2), nullable=True, comment="本次发布使用的调价系数(%)") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizOrder(Base): - """订单主表""" - __tablename__ = "biz_order" - - id = Column(String(32), primary_key=True, comment="主键ID") - order_no = Column(String(50), unique=True, nullable=False, comment="订单编号") - region_id = Column(String(32), nullable=False, comment="区域ID") - client_id = Column(String(32), nullable=False, comment="客户ID") - price_list_id = Column(String(32), nullable=False, comment="价格清单ID") - price_version = Column(Integer, nullable=False, comment="价格版本号") - status = Column(String(30), nullable=False, default="pending_payment", comment="订单状态") - # 三字段状态(已统一为新模型) - order_status = Column(String(20), nullable=False, default="pending_payment", comment="订单主状态: pending_payment/pending_confirm/completed/cancelled") - delivery_status = Column(String(20), nullable=False, default="pending_dispatch", comment="配送状态: pending_dispatch/delivering/delivered") - pay_status = Column(String(20), nullable=False, default="pending_payment", comment="支付状态: pending_payment/paid/refunding/refunded") - cancel_status = Column(String(20), nullable=False, default="none", comment="取消审核状态: none/pending_review/approved/rejected") - payment_method = Column(String(20), nullable=True, comment="支付渠道: online=线上(微信等), offline=线下结算(与 pay_status 独立)") - client_settlement_status = Column(String(20), nullable=False, default="pending", comment="客户结算状态: pending/settled") - supplier_settlement_status = Column(String(20), nullable=False, default="pending", comment="供应商结算状态: pending/settled") - client_settlement_at = Column(DateTime, nullable=True, comment="客户结算时间") - supplier_settlement_at = Column(DateTime, nullable=True, comment="供应商结算时间") - total_amount = Column(Numeric(12, 2), default=0, comment="订单总金额") - paid_amount = Column(Numeric(12, 2), default=0, comment="已付金额") - receivable_amount = Column(Numeric(12, 2), default=0, comment="应收金额") - refund_amount = Column(Numeric(12, 2), default=0, comment="退款金额") - contact_person = Column(String(100), nullable=True, comment="联系人") - contact_phone = Column(String(20), nullable=True, comment="联系电话") - delivery_address = Column(String(500), nullable=True, comment="配送地址") - cancel_reason = Column(String(500), nullable=True, comment="取消原因") - cancel_review_time = Column(DateTime, nullable=True, comment="取消审核时间") - cancel_reviewed_by = Column(String(32), nullable=True, comment="取消审核人ID") - cancel_review_note = Column(String(500), nullable=True, comment="取消审核备注") - completed_time = Column(DateTime, nullable=True, comment="完结时间") - completed_by = Column(String(32), nullable=True, comment="完结人ID") - remark = Column(Text, nullable=True, comment="订单备注") - payment_time = Column(DateTime, nullable=True, comment="支付时间") - delivery_assign_time = Column(DateTime, nullable=True, comment="配送分配时间") - delivered_time = Column(DateTime, nullable=True, comment="配送完成时间(待客户确认收货起算点)") - lock_operator_id = Column(String(32), nullable=True, comment="锁定操作人ID") - lock_time = Column(DateTime, nullable=True, comment="锁定时间") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizOrderItem(Base): - """订单项表""" - __tablename__ = "biz_order_item" - - id = Column(String(32), primary_key=True, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - unit_id = Column(String(32), nullable=False, comment="单位ID") - quantity = Column(Numeric(12, 4), default=0, comment="数量") - unit_price = Column(Numeric(10, 2), nullable=False, comment="单价") - amount = Column(Numeric(12, 2), default=0, comment="金额") - delivered_quantity = Column(Numeric(12, 4), default=0, comment="已配送数量") - status = Column(String(20), default="normal", comment="项状态") - note = Column(String(500), nullable=True, comment="备注") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizOrderHis(Base): - """订单主表历史表""" - __tablename__ = "biz_order_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - order_no = Column(String(50), nullable=False, comment="订单编号") - region_id = Column(String(32), nullable=False, comment="区域ID") - client_id = Column(String(32), nullable=False, comment="客户ID") - price_list_id = Column(String(32), nullable=False, comment="价格清单ID") - price_version = Column(Integer, nullable=False, comment="价格版本号") - status = Column(String(30), nullable=False, default="pending_payment", comment="订单状态") - # 三字段状态(历史表同步保留新模型) - order_status = Column(String(20), nullable=False, default="pending_payment", comment="订单主状态: pending_payment/pending_confirm/completed/cancelled") - delivery_status = Column(String(20), nullable=False, default="pending_dispatch", comment="配送状态: pending_dispatch/delivering/delivered") - pay_status = Column(String(20), nullable=False, default="pending_payment", comment="支付状态: pending_payment/paid/refunding/refunded") - cancel_status = Column(String(20), nullable=False, default="none", comment="取消审核状态: none/pending_review/approved/rejected") - payment_method = Column(String(20), nullable=True, comment="支付渠道: online=线上(微信等), offline=线下结算(与 pay_status 独立)") - client_settlement_status = Column(String(20), nullable=False, default="pending", comment="客户结算状态: pending/settled") - supplier_settlement_status = Column(String(20), nullable=False, default="pending", comment="供应商结算状态: pending/settled") - client_settlement_at = Column(DateTime, nullable=True, comment="客户结算时间") - supplier_settlement_at = Column(DateTime, nullable=True, comment="供应商结算时间") - total_amount = Column(Numeric(12, 2), default=0, comment="订单总金额") - paid_amount = Column(Numeric(12, 2), default=0, comment="已付金额") - receivable_amount = Column(Numeric(12, 2), default=0, comment="应收金额") - refund_amount = Column(Numeric(12, 2), default=0, comment="退款金额") - contact_person = Column(String(100), nullable=True, comment="联系人") - contact_phone = Column(String(20), nullable=True, comment="联系电话") - delivery_address = Column(String(500), nullable=True, comment="配送地址") - cancel_reason = Column(String(500), nullable=True, comment="取消原因") - cancel_review_time = Column(DateTime, nullable=True, comment="取消审核时间") - cancel_reviewed_by = Column(String(32), nullable=True, comment="取消审核人ID") - cancel_review_note = Column(String(500), nullable=True, comment="取消审核备注") - completed_time = Column(DateTime, nullable=True, comment="完结时间") - completed_by = Column(String(32), nullable=True, comment="完结人ID") - remark = Column(Text, nullable=True, comment="订单备注") - payment_time = Column(DateTime, nullable=True, comment="支付时间") - delivery_assign_time = Column(DateTime, nullable=True, comment="配送分配时间") - delivered_time = Column(DateTime, nullable=True, comment="配送完成时间(待客户确认收货起算点)") - lock_operator_id = Column(String(32), nullable=True, comment="锁定操作人ID") - lock_time = Column(DateTime, nullable=True, comment="锁定时间") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizOrderStatusLog(Base): - """订单状态变更日志表""" - __tablename__ = "biz_order_status_log" - - id = Column(String(32), primary_key=True, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - from_status = Column(String(30), nullable=True, comment="变更前状态") - to_status = Column(String(30), nullable=False, comment="变更后状态") - operator_id = Column(String(32), nullable=False, comment="操作人ID") - operator_role = Column(String(20), nullable=False, comment="操作人角色") - reason = Column(String(500), nullable=True, comment="变更原因") - note = Column(String(1000), nullable=True, comment="备注") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizDeliveryTask(Base): - """配送任务表""" - __tablename__ = "biz_delivery_task" - - id = Column(String(32), primary_key=True, comment="主键ID") - task_no = Column(String(50), unique=True, nullable=False, comment="配送单编号") - order_id = Column(String(32), nullable=False, comment="订单ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - supplier_id = Column(String(32), nullable=False, comment="供应商ID") - deliver_id = Column(String(32), nullable=True, comment="配送员ID") - status = Column(String(20), default="pending", comment="状态") - deliverer_status = Column(String(20), default="pending_pickup", comment="配送员状态: pending_pickup/delivering/delivered") - supplier_prepare_status = Column(String(20), default="pending_prepare", comment="供应商备货状态: pending_prepare/prepared/picked_up") - pickup_time = Column(DateTime, nullable=True, comment="取货时间") - pickup_remark = Column(String(500), nullable=True, comment="取货备注") - pickup_image_url = Column(String(500), nullable=True, comment="取货凭证图片") - delivery_time = Column(DateTime, nullable=True, comment="送达时间") - delivery_remark = Column(String(500), nullable=True, comment="送达备注") - delivery_image_url = Column(String(500), nullable=True, comment="送达凭证图片") - expire_warning_sent = Column(Boolean, default=False, comment="是否已发送过期预警") - effective_time = Column(DateTime, nullable=True, comment="生效时间") - dispatch_remark = Column(String(500), nullable=True, comment="运营分配配送备注") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizDeliveryTaskItem(Base): - """配送任务明细表""" - __tablename__ = "biz_delivery_task_item" - - id = Column(String(32), primary_key=True, comment="主键ID") - task_id = Column(String(32), nullable=False, comment="配送任务ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - order_item_id = Column(String(32), nullable=False, comment="订单项ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - client_id = Column(String(32), nullable=False, comment="客户ID") - delivery_address = Column(String(500), nullable=False, comment="配送地址") - contact_person = Column(String(100), nullable=False, comment="联系人") - contact_phone = Column(String(20), nullable=False, comment="联系电话") - quantity = Column(Numeric(12, 4), default=0, comment="配送数量") - delivered_quantity = Column(Numeric(12, 4), default=0, comment="已送达数量") - status = Column(String(20), default="pending", comment="状态") - pickup_time = Column(DateTime, nullable=True, comment="取货时间") - delivery_time = Column(DateTime, nullable=True, comment="送达时间") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizDeliveryTaskHis(Base): - """配送任务历史表""" - __tablename__ = "biz_delivery_task_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - task_no = Column(String(50), nullable=False, comment="配送单编号") - order_id = Column(String(32), nullable=False, comment="订单ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - supplier_id = Column(String(32), nullable=False, comment="供应商ID") - deliver_id = Column(String(32), nullable=True, comment="配送员ID") - status = Column(String(20), default="pending", comment="状态") - deliverer_status = Column(String(20), default="pending_pickup", comment="配送员状态: pending_pickup/delivering/delivered") - supplier_prepare_status = Column(String(20), default="pending_prepare", comment="供应商备货状态: pending_prepare/prepared/picked_up") - pickup_time = Column(DateTime, nullable=True, comment="取货时间") - pickup_remark = Column(String(500), nullable=True, comment="取货备注") - pickup_image_url = Column(String(500), nullable=True, comment="取货凭证图片") - delivery_time = Column(DateTime, nullable=True, comment="送达时间") - delivery_remark = Column(String(500), nullable=True, comment="送达备注") - delivery_image_url = Column(String(500), nullable=True, comment="送达凭证图片") - expire_warning_sent = Column(Boolean, default=False, comment="是否已发送过期预警") - effective_time = Column(DateTime, nullable=True, comment="生效时间") - dispatch_remark = Column(String(500), nullable=True, comment="运营分配配送备注") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizSettlement(Base): - """结算记录表""" - __tablename__ = "biz_settlement" - - id = Column(String(32), primary_key=True, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - client_id = Column(String(32), nullable=False, comment="客户ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - amount = Column(Numeric(12, 2), nullable=False, comment="结算金额") - settlement_method = Column(String(20), nullable=False, comment="结算方式") - settlement_time = Column(DateTime, nullable=True, comment="结算时间") - settlement_file_url = Column(String(500), nullable=True, comment="结算凭证文件URL") - settlement_note = Column(String(500), nullable=True, comment="结算备注") - status = Column(String(20), default="pending", comment="状态") - reviewed_by = Column(String(32), nullable=True, comment="审核人ID") - reviewed_time = Column(DateTime, nullable=True, comment="审核时间") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class OrdClientSettlement(Base): - """客户结算主表""" - __tablename__ = "ord_client_settlement" - - id = Column(String(32), primary_key=True, comment="主键ID") - settlement_no = Column(String(50), nullable=False, unique=True, comment="结算单号") - client_id = Column(String(32), nullable=False, comment="客户ID") - client_name = Column(String(100), nullable=True, comment="客户名称快照") - order_count = Column(Integer, nullable=False, default=0, comment="订单数量") - total_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="结算总金额") - voucher_urls = Column(Text, nullable=True, comment="结算凭证URL(JSON数组)") - remark = Column(String(500), nullable=True, comment="结算备注") - settlement_at = Column(DateTime, nullable=False, default=datetime.now, comment="结算时间") - status = Column(String(20), nullable=False, default="done", comment="结算单状态") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class OrdClientSettlementHis(Base): - """客户结算主表历史""" - __tablename__ = "ord_client_settlement_his" - __table_args__ = (PrimaryKeyConstraint("id", "his_create_at", name="pk_ord_client_settlement_his"),) - - id = Column(String(32), nullable=False, comment="主键ID") - settlement_no = Column(String(50), nullable=False, comment="结算单号") - client_id = Column(String(32), nullable=False, comment="客户ID") - client_name = Column(String(100), nullable=True, comment="客户名称快照") - order_count = Column(Integer, nullable=False, default=0, comment="订单数量") - total_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="结算总金额") - voucher_urls = Column(Text, nullable=True, comment="结算凭证URL(JSON数组)") - remark = Column(String(500), nullable=True, comment="结算备注") - settlement_at = Column(DateTime, nullable=False, default=datetime.now, comment="结算时间") - status = Column(String(20), nullable=False, default="done", comment="结算单状态") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class OrdClientSettlementItem(Base): - """客户结算明细表""" - __tablename__ = "ord_client_settlement_item" - - id = Column(String(32), primary_key=True, comment="主键ID") - settlement_id = Column(String(32), nullable=False, comment="结算主表ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - order_no = Column(String(50), nullable=False, comment="订单号") - client_id = Column(String(32), nullable=False, comment="客户ID") - order_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="订单结算金额") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class OrdClientSettlementItemHis(Base): - """客户结算明细历史表""" - __tablename__ = "ord_client_settlement_item_his" - __table_args__ = (PrimaryKeyConstraint("id", "his_create_at", name="pk_ord_client_settlement_item_his"),) - - id = Column(String(32), nullable=False, comment="主键ID") - settlement_id = Column(String(32), nullable=False, comment="结算主表ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - order_no = Column(String(50), nullable=False, comment="订单号") - client_id = Column(String(32), nullable=False, comment="客户ID") - order_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="订单结算金额") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class OrdSupplierSettlement(Base): - """供应商结算主表""" - __tablename__ = "ord_supplier_settlement" - - id = Column(String(32), primary_key=True, comment="主键ID") - settlement_no = Column(String(50), nullable=False, unique=True, comment="结算单号") - supplier_id = Column(String(32), nullable=False, comment="供应商ID") - supplier_name = Column(String(100), nullable=True, comment="供应商名称快照") - order_count = Column(Integer, nullable=False, default=0, comment="订单数量") - total_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="结算总金额") - voucher_urls = Column(Text, nullable=True, comment="结算凭证URL(JSON数组)") - remark = Column(String(500), nullable=True, comment="结算备注") - settlement_at = Column(DateTime, nullable=False, default=datetime.now, comment="结算时间") - status = Column(String(20), nullable=False, default="done", comment="结算单状态") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class OrdSupplierSettlementHis(Base): - """供应商结算主表历史""" - __tablename__ = "ord_supplier_settlement_his" - __table_args__ = (PrimaryKeyConstraint("id", "his_create_at", name="pk_ord_supplier_settlement_his"),) - - id = Column(String(32), nullable=False, comment="主键ID") - settlement_no = Column(String(50), nullable=False, comment="结算单号") - supplier_id = Column(String(32), nullable=False, comment="供应商ID") - supplier_name = Column(String(100), nullable=True, comment="供应商名称快照") - order_count = Column(Integer, nullable=False, default=0, comment="订单数量") - total_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="结算总金额") - voucher_urls = Column(Text, nullable=True, comment="结算凭证URL(JSON数组)") - remark = Column(String(500), nullable=True, comment="结算备注") - settlement_at = Column(DateTime, nullable=False, default=datetime.now, comment="结算时间") - status = Column(String(20), nullable=False, default="done", comment="结算单状态") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class OrdSupplierSettlementItem(Base): - """供应商结算明细表""" - __tablename__ = "ord_supplier_settlement_item" - - id = Column(String(32), primary_key=True, comment="主键ID") - settlement_id = Column(String(32), nullable=False, comment="结算主表ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - order_no = Column(String(50), nullable=False, comment="订单号") - supplier_id = Column(String(32), nullable=False, comment="供应商ID") - order_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="订单结算金额") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class OrdSupplierSettlementItemHis(Base): - """供应商结算明细历史表""" - __tablename__ = "ord_supplier_settlement_item_his" - __table_args__ = (PrimaryKeyConstraint("id", "his_create_at", name="pk_ord_supplier_settlement_item_his"),) - - id = Column(String(32), nullable=False, comment="主键ID") - settlement_id = Column(String(32), nullable=False, comment="结算主表ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - order_no = Column(String(50), nullable=False, comment="订单号") - supplier_id = Column(String(32), nullable=False, comment="供应商ID") - order_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="订单结算金额") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizRefund(Base): - """退款记录表""" - __tablename__ = "biz_refund" - - id = Column(String(32), primary_key=True, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - refund_no = Column(String(50), unique=True, nullable=False, comment="退款单编号") - refund_amount = Column(Numeric(12, 2), nullable=False, comment="退款金额") - refund_method = Column(String(20), nullable=False, comment="退款方式") - refund_no_wechat = Column(String(100), nullable=True, comment="微信退款单号") - refund_time = Column(DateTime, nullable=True, comment="退款时间") - refund_result = Column(String(20), default="success", comment="退款结果") - refund_fail_reason = Column(String(500), nullable=True, comment="退款失败原因") - status = Column(String(20), default="pending", comment="状态") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizSettlementHis(Base): - """结算记录历史表""" - __tablename__ = "biz_settlement_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - client_id = Column(String(32), nullable=False, comment="客户ID") - region_id = Column(String(32), nullable=False, comment="区域ID") - amount = Column(Numeric(12, 2), nullable=False, comment="结算金额") - settlement_method = Column(String(20), nullable=False, comment="结算方式") - settlement_time = Column(DateTime, nullable=True, comment="结算时间") - settlement_file_url = Column(String(500), nullable=True, comment="结算凭证文件URL") - settlement_note = Column(String(500), nullable=True, comment="结算备注") - status = Column(String(20), default="pending", comment="状态") - reviewed_by = Column(String(32), nullable=True, comment="审核人ID") - reviewed_time = Column(DateTime, nullable=True, comment="审核时间") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizRefundHis(Base): - """退款记录历史表""" - __tablename__ = "biz_refund_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - refund_no = Column(String(50), nullable=False, comment="退款单编号") - refund_amount = Column(Numeric(12, 2), nullable=False, comment="退款金额") - refund_method = Column(String(20), nullable=False, comment="退款方式") - refund_no_wechat = Column(String(100), nullable=True, comment="微信退款单号") - refund_time = Column(DateTime, nullable=True, comment="退款时间") - refund_result = Column(String(20), default="success", comment="退款结果") - refund_fail_reason = Column(String(500), nullable=True, comment="退款失败原因") - status = Column(String(20), default="pending", comment="状态") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizWechatPay(Base): - """微信支付订单表""" - __tablename__ = "biz_wechat_pay" - - id = Column(String(32), primary_key=True, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - out_trade_no = Column(String(64), unique=True, nullable=False, comment="商户订单号") - transaction_id = Column(String(64), nullable=True, comment="微信支付订单号") - total_fee = Column(Integer, nullable=False, comment="支付金额(分)") - cash_fee = Column(Integer, nullable=True, comment="现金支付金额(分)") - pay_time = Column(DateTime, nullable=True, comment="支付时间") - trade_state = Column(String(20), nullable=True, comment="交易状态") - trade_state_desc = Column(String(200), nullable=True, comment="交易状态描述") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizAttachment(Base): - """附件表""" - __tablename__ = "biz_attachment" - - id = Column(String(32), primary_key=True, comment="主键ID") - business_type = Column(String(50), nullable=False, comment="业务类型") - business_id = Column(String(32), nullable=False, comment="业务ID") - file_name = Column(String(200), nullable=False, comment="文件名称") - file_url = Column(String(500), nullable=False, comment="文件URL") - file_size = Column(Integer, nullable=True, comment="文件大小") - file_type = Column(String(50), nullable=True, comment="文件类型") - upload_by = Column(String(32), nullable=True, comment="上传人ID") - upload_by_name = Column(String(100), nullable=True, comment="上传人姓名") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizOrderAdjust(Base): - """订单调整记录表""" - __tablename__ = "biz_order_adjust" - - id = Column(String(32), primary_key=True, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - order_item_id = Column(String(32), nullable=True, comment="订单项ID") - adjust_type = Column(String(20), nullable=False, comment="调整类型") - before_quantity = Column(Numeric(12, 4), nullable=True, comment="调整前数量") - after_quantity = Column(Numeric(12, 4), nullable=True, comment="调整后数量") - before_amount = Column(Numeric(12, 2), nullable=True, comment="调整前金额") - after_amount = Column(Numeric(12, 2), nullable=True, comment="调整后金额") - difference_amount = Column(Numeric(12, 2), default=0, comment="差异金额") - reason = Column(String(500), nullable=False, comment="调整原因") - snapshot_items_json = Column(Text, nullable=True, comment="调整后订单商品快照(JSON)") - substitute_product_id = Column(String(32), nullable=True, comment="替代商品ID") - operator_id = Column(String(32), nullable=False, comment="操作人ID") - operator_name = Column(String(100), nullable=False, comment="操作人姓名") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizOrderAdjustHis(Base): - """订单调整记录历史表""" - __tablename__ = "biz_order_adjust_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - order_item_id = Column(String(32), nullable=True, comment="订单项ID") - adjust_type = Column(String(20), nullable=False, comment="调整类型") - before_quantity = Column(Numeric(12, 4), nullable=True, comment="调整前数量") - after_quantity = Column(Numeric(12, 4), nullable=True, comment="调整后数量") - before_amount = Column(Numeric(12, 2), nullable=True, comment="调整前金额") - after_amount = Column(Numeric(12, 2), nullable=True, comment="调整后金额") - difference_amount = Column(Numeric(12, 2), default=0, comment="差异金额") - reason = Column(String(500), nullable=False, comment="调整原因") - snapshot_items_json = Column(Text, nullable=True, comment="调整后订单商品快照(JSON)") - substitute_product_id = Column(String(32), nullable=True, comment="替代商品ID") - operator_id = Column(String(32), nullable=False, comment="操作人ID") - operator_name = Column(String(100), nullable=False, comment="操作人姓名") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizInvoice(Base): - """发票表""" - __tablename__ = "biz_invoice" - - id = Column(String(32), primary_key=True, comment="主键ID") - invoice_no = Column(String(50), nullable=False, unique=True, comment="发票号") - order_id = Column(String(32), nullable=False, comment="订单ID") - order_no = Column(String(50), nullable=False, comment="订单号") - client_id = Column(String(32), nullable=False, comment="客户ID") - client_name = Column(String(100), nullable=False, comment="客户名称") - invoice_type = Column(String(20), nullable=False, comment="发票类型: normal-普通发票, vat-增值税发票") - invoice_amount = Column(Numeric(12, 2), nullable=False, comment="发票金额") - tax_rate = Column(Numeric(5, 2), default=0, comment="税率") - tax_amount = Column(Numeric(12, 2), default=0, comment="税额") - title = Column(String(200), nullable=False, comment="发票抬头") - tax_no = Column(String(50), nullable=True, comment="税号") - address = Column(String(500), nullable=True, comment="开票地址") - phone = Column(String(20), nullable=True, comment="电话") - bank = Column(String(100), nullable=True, comment="开户银行") - bank_account = Column(String(50), nullable=True, comment="银行账号") - status = Column(String(20), nullable=False, default="pending", comment="状态: pending-待开票, issued-已开票, void-已作废") - invoice_url = Column(String(500), nullable=True, comment="发票文件URL") - issued_at = Column(DateTime, nullable=True, comment="开票时间") - issued_by = Column(String(32), nullable=True, comment="开票人ID") - issued_by_name = Column(String(100), nullable=True, comment="开票人姓名") - remark = Column(String(500), nullable=True, comment="备注") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizSystemConfig(Base): - """系统配置表""" - __tablename__ = "biz_system_config" - - id = Column(String(32), primary_key=True, comment="主键ID") - config_key = Column(String(100), nullable=False, unique=True, comment="配置键") - config_value = Column(Text, nullable=True, comment="配置值") - config_type = Column(String(50), default="string", comment="配置类型: string, number, boolean, json") - config_name = Column(String(100), nullable=False, comment="配置名称") - config_group = Column(String(50), nullable=True, comment="配置分组") - description = Column(String(500), nullable=True, comment="描述") - is_active = Column(Integer, default=1, comment="是否启用") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizDelivererReview(Base): - """配送员评价表""" - __tablename__ = "biz_deliverer_review" - - id = Column(String(32), primary_key=True, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - task_id = Column(String(32), nullable=True, comment="配送任务ID") - client_id = Column(String(32), nullable=False, comment="客户ID") - deliverer_id = Column(String(32), nullable=False, comment="配送员ID") - score = Column(Integer, nullable=False, comment="评分(1-5)") - tags = Column(String(500), nullable=True, comment="评价标签,逗号分隔") - content = Column(String(1000), nullable=True, comment="评价内容") - is_anonymous = Column(Boolean, default=False, comment="是否匿名") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizDelivererReviewHis(Base): - """配送员评价历史表""" - __tablename__ = "biz_deliverer_review_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - task_id = Column(String(32), nullable=True, comment="配送任务ID") - client_id = Column(String(32), nullable=False, comment="客户ID") - deliverer_id = Column(String(32), nullable=False, comment="配送员ID") - score = Column(Integer, nullable=False, comment="评分(1-5)") - tags = Column(String(500), nullable=True, comment="评价标签,逗号分隔") - content = Column(String(1000), nullable=True, comment="评价内容") - is_anonymous = Column(Boolean, default=False, comment="是否匿名") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizDeliveryWarning(Base): - """配送时效预警表""" - __tablename__ = "biz_delivery_warning" - - id = Column(String(32), primary_key=True, comment="主键ID") - task_id = Column(String(32), nullable=False, comment="配送任务ID") - order_id = Column(String(32), nullable=True, comment="订单ID") - warning_type = Column(String(20), nullable=False, comment="预警类型") - urgency_level = Column(String(20), nullable=False, comment="紧急程度") - remaining_minutes = Column(Integer, nullable=True, comment="剩余分钟数") - message = Column(String(500), nullable=True, comment="预警消息") - is_sent = Column(Integer, default=0, comment="是否已发送通知") - sent_time = Column(DateTime, nullable=True, comment="发送时间") - is_resolved = Column(Integer, default=0, comment="是否已解决") - resolved_time = Column(DateTime, nullable=True, comment="解决时间") - resolved_by = Column(String(32), nullable=True, comment="解决人ID") - resolved_note = Column(String(500), nullable=True, comment="解决备注") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - is_del = Column(Integer, default=0, comment="是否删除") - - -class BizDeliveryWarningHis(Base): - """配送时效预警历史表""" - __tablename__ = "biz_delivery_warning_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - task_id = Column(String(32), nullable=False, comment="配送任务ID") - order_id = Column(String(32), nullable=True, comment="订单ID") - warning_type = Column(String(20), nullable=False, comment="预警类型") - urgency_level = Column(String(20), nullable=False, comment="紧急程度") - remaining_minutes = Column(Integer, nullable=True, comment="剩余分钟数") - message = Column(String(500), nullable=True, comment="预警消息") - is_sent = Column(Integer, default=0, comment="是否已发送通知") - sent_time = Column(DateTime, nullable=True, comment="发送时间") - is_resolved = Column(Integer, default=0, comment="是否已解决") - resolved_time = Column(DateTime, nullable=True, comment="解决时间") - resolved_by = Column(String(32), nullable=True, comment="解决人ID") - resolved_note = Column(String(500), nullable=True, comment="解决备注") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class BizOrderStatusLogHis(Base): - """订单状态变更日志历史表""" - __tablename__ = "biz_order_status_log_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - order_id = Column(String(32), nullable=False, comment="订单ID") - from_status = Column(String(30), nullable=True, comment="变更前状态") - to_status = Column(String(30), nullable=False, comment="变更后状态") - operator_id = Column(String(32), nullable=False, comment="操作人ID") - operator_role = Column(String(20), nullable=False, comment="操作人角色") - reason = Column(String(500), nullable=True, comment="变更原因") - note = Column(String(1000), nullable=True, comment="备注") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - class SysApiRequestLogHis(Base): """API 请求日志历史表""" + __tablename__ = "sys_api_request_log_his" - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") + id = Column(String(64), primary_key=True, nullable=False, comment="主键ID") http_method = Column(String(10), nullable=False, comment="HTTP 方法") path = Column(String(500), nullable=False, comment="请求路径") query_string = Column(String(2000), nullable=True, comment="Query 字符串") @@ -1410,9 +193,9 @@ class SysApiRequestLogHis(Base): response_body = Column(Text, nullable=True, comment="响应体(截断)") status_code = Column(Integer, nullable=False, comment="HTTP 状态码") duration_ms = Column(Integer, nullable=False, comment="耗时(ms)") - user_id = Column(String(32), nullable=True, comment="用户ID") + user_id = Column(String(64), nullable=True, comment="用户ID") username = Column(String(100), nullable=True, comment="用户名") - role = Column(String(20), nullable=True, comment="角色") + role = Column(String(64), nullable=True, comment="角色") ip_address = Column(String(50), nullable=True, comment="IP地址") user_agent = Column(String(500), nullable=True, comment="User-Agent") create_at = Column(DateTime, default=datetime.now, comment="创建时间") @@ -1421,130 +204,3 @@ class SysApiRequestLogHis(Base): update_at = Column(DateTime, nullable=True, comment="修改时间") update_by = Column(String(32), nullable=True, comment="修改人ID") his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class SysPasswordHistoryHis(Base): - """密码修改历史表(历史)""" - __tablename__ = "sys_password_history_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - user_id = Column(String(32), nullable=False, comment="用户ID") - operation_type = Column(String(20), nullable=False, comment="操作类型") - operator_id = Column(String(32), nullable=True, comment="操作人ID(重置时)") - operator_name = Column(String(100), nullable=True, comment="操作人姓名") - ip_address = Column(String(50), nullable=True, comment="IP地址") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -# ========================================= -# 扩展模块 -# ========================================= - -class SysUpgradeLog(Base): - """系统更新日志表""" - __tablename__ = "sys_upgrade_log" - - id = Column(String(32), primary_key=True, comment="主键ID") - version = Column(String(50), nullable=False, comment="版本号") - title = Column(String(200), nullable=False, comment="更新标题") - date = Column(DateTime, nullable=False, comment="更新日期") - detail = Column(Text, nullable=True, comment="更新详情(JSON数组)") - require_re_login = Column(Integer, default=0, comment="是否需要重新登录: 0-否, 1-是") - remark = Column(String(500), nullable=True, comment="备注") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class SysUpgradeLogHis(Base): - """系统更新日志历史表""" - __tablename__ = "sys_upgrade_log_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - version = Column(String(50), nullable=False, comment="版本号") - title = Column(String(200), nullable=False, comment="更新标题") - date = Column(DateTime, nullable=False, comment="更新日期") - detail = Column(Text, nullable=True, comment="更新详情(JSON数组)") - require_re_login = Column(Integer, default=0, comment="是否需要重新登录: 0-否, 1-是") - remark = Column(String(500), nullable=True, comment="备注") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") - - -class SysComment(Base): - """系统评论表""" - __tablename__ = "sys_comment" - - id = Column(String(32), primary_key=True, comment="主键ID") - parent_id = Column(String(32), nullable=True, comment="父评论ID") - author = Column(String(100), nullable=False, comment="评论者") - author_id = Column(String(32), nullable=True, comment="评论者ID") - content = Column(String(1000), nullable=False, comment="评论内容") - target_type = Column(String(50), nullable=True, comment="目标类型") - target_id = Column(String(32), nullable=True, comment="目标ID") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizClientCart(Base): - """客户购物车""" - __tablename__ = "biz_client_cart" - - id = Column(String(32), primary_key=True, comment="主键ID") - client_id = Column(String(32), nullable=False, comment="客户用户ID") - price_list_id = Column(String(32), nullable=False, comment="价格清单ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - unit_id = Column(String(32), nullable=False, comment="单位ID") - quantity = Column(Numeric(12, 4), nullable=False, default=0, comment="数量") - unit_price = Column(Numeric(12, 2), nullable=False, default=0, comment="单价(元)") - amount = Column(Numeric(12, 2), nullable=False, default=0, comment="行金额(元)") - product_name = Column(String(200), nullable=True, comment="商品名称快照") - unit_name = Column(String(50), nullable=True, comment="单位名称快照") - category_name = Column(String(100), nullable=True, comment="分类名称快照") - image_url = Column(String(500), nullable=True, comment="图片") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizClientCartHis(Base): - """客户购物车历史""" - __tablename__ = "biz_client_cart_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - client_id = Column(String(32), nullable=False, comment="客户用户ID") - price_list_id = Column(String(32), nullable=False, comment="价格清单ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - unit_id = Column(String(32), nullable=False, comment="单位ID") - quantity = Column(Numeric(12, 4), nullable=False, default=0, comment="数量") - unit_price = Column(Numeric(12, 2), nullable=False, default=0, comment="单价(元)") - amount = Column(Numeric(12, 2), nullable=False, default=0, comment="行金额(元)") - product_name = Column(String(200), nullable=True, comment="商品名称快照") - unit_name = Column(String(50), nullable=True, comment="单位名称快照") - category_name = Column(String(100), nullable=True, comment="分类名称快照") - image_url = Column(String(500), nullable=True, comment="图片") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") diff --git a/backend/app/models/order.py b/backend/app/models/order.py deleted file mode 100644 index ebca4c60..00000000 --- a/backend/app/models/order.py +++ /dev/null @@ -1,166 +0,0 @@ -"""订单相关模型""" -from datetime import datetime - -from sqlalchemy import Boolean, Column, DateTime, Integer, String, Enum as SQLEnum, Numeric, Text, ForeignKey -from sqlalchemy.orm import relationship - -from app.core.database import Base -import enum - - -class OrderStatus(str, enum.Enum): - """订单主状态""" - PENDING_PAYMENT = "pending_payment" # 待支付 - PENDING_CONFIRM = "pending_confirm" # 待客户确认收货 - COMPLETED = "completed" # 已完结 - CANCELLED = "cancelled" # 已取消 - - -class PaymentMethod(str, enum.Enum): - """支付方式""" - WECHAT = "wechat" # 微信支付 - OFFLINE = "offline" # 线下结算 - - -class Order(Base): - """订单主表""" - __tablename__ = "orders" - - id = Column(Integer, primary_key=True, index=True, autoincrement=True) - order_no = Column(String(50), unique=True, nullable=False, comment="订单编号") - region_id = Column(Integer, nullable=False, comment="区域ID") - client_id = Column(Integer, nullable=False, comment="客户ID") - price_list_id = Column(Integer, nullable=False, comment="使用的价格清单ID") - price_version = Column(Integer, nullable=False, comment="价格版本号") - - status = Column(SQLEnum(OrderStatus), default=OrderStatus.PENDING_PAYMENT, comment="订单状态") - payment_method = Column(SQLEnum(PaymentMethod), nullable=True, comment="支付方式") - - total_amount = Column(Numeric(12, 2), default=0, comment="订单总金额") - paid_amount = Column(Numeric(12, 2), default=0, comment="已付金额") - receivable_amount = Column(Numeric(12, 2), default=0, comment="应收金额") - refund_amount = Column(Numeric(12, 2), default=0, comment="退款金额") - - contact_person = Column(String(100), nullable=True, comment="联系人") - contact_phone = Column(String(20), nullable=True, comment="联系电话") - delivery_address = Column(String(500), nullable=True, comment="配送地址") - - cancel_reason = Column(String(500), nullable=True, comment="取消原因") - cancel_review_time = Column(DateTime, nullable=True, comment="取消审核时间") - cancel_reviewed_by = Column(Integer, nullable=True, comment="取消审核人ID") - cancel_review_note = Column(String(500), nullable=True, comment="取消审核备注") - - completed_time = Column(DateTime, nullable=True, comment="完结时间") - completed_by = Column(Integer, nullable=True, comment="完结人ID") - - remark = Column(Text, nullable=True, comment="订单备注") - - payment_time = Column(DateTime, nullable=True, comment="支付时间") - delivery_assign_time = Column(DateTime, nullable=True, comment="配送分配时间") - - created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间") - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间") - - # 关联 - client = relationship("Client", back_populates="orders") - items = relationship("OrderItem", back_populates="order") - delivery_task = relationship("DeliveryTask", back_populates="order", uselist=False) - status_logs = relationship("OrderStatusLog", back_populates="order") - - -class OrderItem(Base): - """订单项表""" - __tablename__ = "order_items" - - id = Column(Integer, primary_key=True, index=True, autoincrement=True) - order_id = Column(Integer, nullable=False, comment="订单ID") - product_id = Column(Integer, nullable=False, comment="商品ID") - unit_id = Column(Integer, nullable=False, comment="单位ID") - - quantity = Column(Numeric(12, 4), default=0, comment="数量") - unit_price = Column(Numeric(10, 2), nullable=False, comment="单价") - amount = Column(Numeric(12, 2), default=0, comment="金额") - - delivered_quantity = Column(Numeric(12, 4), default=0, comment="已配送数量") - - status = Column(String(20), default="normal", comment="项状态:normal-正常,reduced-已减量,deleted-已删除,substituted-已替代") - note = Column(String(500), nullable=True, comment="备注(如缺货原因)") - - created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间") - - order = relationship("Order", back_populates="items") - product = relationship("Product", back_populates="order_items") - - -class OrderStatusLog(Base): - """订单状态变更日志""" - __tablename__ = "order_status_logs" - - id = Column(Integer, primary_key=True, index=True, autoincrement=True) - order_id = Column(Integer, nullable=False, comment="订单ID") - from_status = Column(String(30), nullable=True, comment="变更前状态") - to_status = Column(String(30), nullable=False, comment="变更后状态") - operator_id = Column(Integer, nullable=False, comment="操作人ID") - operator_role = Column(String(20), nullable=False, comment="操作人角色") - reason = Column(String(500), nullable=True, comment="变更原因") - note = Column(String(1000), nullable=True, comment="备注") - created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间") - - order = relationship("Order", back_populates="status_logs") - - -class DeliveryTask(Base): - """配送任务表""" - __tablename__ = "delivery_tasks" - - id = Column(Integer, primary_key=True, index=True, autoincrement=True) - task_no = Column(String(50), unique=True, nullable=False, comment="配送单编号") - order_id = Column(Integer, nullable=False, comment="订单ID") - region_id = Column(Integer, nullable=False, comment="区域ID") - supplier_id = Column(Integer, nullable=False, comment="供应商ID") - deliver_id = Column(Integer, nullable=True, comment="配送员ID") - - status = Column(String(20), default="pending", comment="状态:pending-待取货,picking-取货中,delivering-配送中,completed-已完成,cancelled-已取消") - deliverer_status = Column(String(20), default="pending_pickup", comment="配送员状态: pending_pickup/delivering/delivered") - supplier_prepare_status = Column(String(20), default="pending_prepare", comment="供应商备货状态: pending_prepare/prepared/picked_up") - - pickup_time = Column(DateTime, nullable=True, comment="取货时间") - pickup_remark = Column(String(500), nullable=True, comment="取货备注") - pickup_image_url = Column(String(500), nullable=True, comment="取货凭证图片") - - delivery_time = Column(DateTime, nullable=True, comment="送达时间") - delivery_remark = Column(String(500), nullable=True, comment="送达备注") - delivery_image_url = Column(String(500), nullable=True, comment="送达凭证图片") - - expire_warning_sent = Column(Boolean, default=False, comment="是否已发送过期预警") - - created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间") - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间") - - # 关联 - order = relationship("Order", back_populates="delivery_task") - supplier = relationship("Supplier", back_populates="delivery_tasks") - deliver = relationship("Deliver", back_populates="delivery_tasks") - - -class Settlement(Base): - """线下结算记录表""" - __tablename__ = "settlements" - - id = Column(Integer, primary_key=True, index=True, autoincrement=True) - order_id = Column(Integer, nullable=False, comment="订单ID") - client_id = Column(Integer, nullable=False, comment="客户ID") - region_id = Column(Integer, nullable=False, comment="区域ID") - - amount = Column(Numeric(12, 2), nullable=False, comment="结算金额") - settlement_method = Column(String(20), nullable=False, comment="结算方式:transfer-转账,cash-现金,other-其他") - settlement_time = Column(DateTime, nullable=True, comment="结算时间") - settlement_file_url = Column(String(500), nullable=True, comment="结算凭证文件URL") - settlement_note = Column(String(500), nullable=True, comment="结算备注") - - status = Column(String(20), default="pending", comment="状态:pending-待确认,completed-已完成") - reviewed_by = Column(Integer, nullable=True, comment="审核人ID") - reviewed_time = Column(DateTime, nullable=True, comment="审核时间") - - created_by = Column(Integer, nullable=False, comment="创建人ID") - created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间") diff --git a/backend/app/models/product_banner.py b/backend/app/models/product_banner.py deleted file mode 100644 index 4bed8a22..00000000 --- a/backend/app/models/product_banner.py +++ /dev/null @@ -1,42 +0,0 @@ -"""商品轮播图模型""" -from datetime import datetime - -from sqlalchemy import Column, String, DateTime, Integer, Text - -from app.core.database import Base - - -class BasProductBanner(Base): - """商品轮播图表""" - __tablename__ = "bas_product_banner" - - id = Column(String(32), primary_key=True, comment="主键ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - media_type = Column(String(20), nullable=False, default="image", comment="媒体类型:image-图片, video-视频") - file_url = Column(String(500), nullable=False, comment="文件URL地址") - thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL") - sort_order = Column(Integer, default=0, comment="排序") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BasProductBannerHis(Base): - """商品轮播图历史表""" - __tablename__ = "bas_product_banner_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - product_id = Column(String(32), nullable=True, comment="商品ID") - media_type = Column(String(20), default="image", comment="媒体类型") - file_url = Column(String(500), nullable=True, comment="文件URL地址") - thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL") - sort_order = Column(Integer, default=0, comment="排序") - create_at = Column(DateTime, nullable=True, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") diff --git a/backend/app/models/product_detail_media.py b/backend/app/models/product_detail_media.py deleted file mode 100644 index 7080d219..00000000 --- a/backend/app/models/product_detail_media.py +++ /dev/null @@ -1,46 +0,0 @@ -"""商品详情媒体模型""" -from datetime import datetime - -from sqlalchemy import Column, String, DateTime, Integer, Text - -from app.core.database import Base - - -class BasProductDetailMedia(Base): - """商品详情媒体表""" - __tablename__ = "bas_product_detail_media" - - id = Column(String(32), primary_key=True, comment="主键ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - content_type = Column(String(20), nullable=False, default="image", comment="内容类型:image-图片, video-视频, text-文本") - content_text = Column(Text, nullable=True, comment="文本内容") - file_url = Column(String(500), nullable=True, comment="文件URL地址") - thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL") - description = Column(String(500), nullable=True, comment="描述") - sort_order = Column(Integer, default=0, comment="排序") - is_del = Column(Integer, default=0, comment="是否删除") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BasProductDetailMediaHis(Base): - """商品详情媒体历史表""" - __tablename__ = "bas_product_detail_media_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - product_id = Column(String(32), nullable=True, comment="商品ID") - content_type = Column(String(20), default="image", comment="内容类型") - content_text = Column(Text, nullable=True, comment="文本内容") - file_url = Column(String(500), nullable=True, comment="文件URL地址") - thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL") - description = Column(String(500), nullable=True, comment="描述") - sort_order = Column(Integer, default=0, comment="排序") - create_at = Column(DateTime, nullable=True, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") diff --git a/backend/app/models/product_parameter.py b/backend/app/models/product_parameter.py deleted file mode 100644 index 5c87b12c..00000000 --- a/backend/app/models/product_parameter.py +++ /dev/null @@ -1,50 +0,0 @@ -"""商品参数码表模型""" -from datetime import datetime - -from sqlalchemy import Column, String, DateTime, Integer, Boolean, Text - -from app.core.database import Base - - -class BasProductParameter(Base): - """商品参数码表""" - __tablename__ = "bas_product_parameter" - - id = Column(String(32), primary_key=True, comment="主键ID") - name = Column(String(100), nullable=False, comment="参数名称") - code = Column(String(50), nullable=False, unique=True, comment="参数编码") - param_type = Column(String(20), nullable=False, default="text", comment="参数类型:text-文本输入, select-单选下拉, multi_select-多选") - options = Column(Text, nullable=True, comment="可选值列表,JSON数组") - placeholder = Column(String(100), nullable=True, comment="输入提示文字") - is_required = Column(Integer, default=0, comment="是否必填:0-否, 1-是") - is_active = Column(Integer, default=1, comment="是否启用:0-禁用, 1-启用") - is_del = Column(Integer, default=0, comment="是否删除:0-否, 1-是") - sort_order = Column(Integer, default=0, comment="排序") - remark = Column(String(500), nullable=True, comment="备注说明") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BasProductParameterHis(Base): - """商品参数码表历史表""" - __tablename__ = "bas_product_parameter_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - name = Column(String(100), nullable=True, comment="参数名称") - code = Column(String(50), nullable=True, comment="参数编码") - param_type = Column(String(20), default="text", comment="参数类型") - options = Column(Text, nullable=True, comment="可选值列表") - placeholder = Column(String(100), nullable=True, comment="输入提示文字") - is_required = Column(Integer, default=0, comment="是否必填") - is_active = Column(Integer, default=1, comment="是否启用") - sort_order = Column(Integer, default=0, comment="排序") - remark = Column(String(500), nullable=True, comment="备注说明") - create_at = Column(DateTime, nullable=True, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") diff --git a/backend/app/models/product_parameter_value.py b/backend/app/models/product_parameter_value.py deleted file mode 100644 index b3e6dfe0..00000000 --- a/backend/app/models/product_parameter_value.py +++ /dev/null @@ -1,46 +0,0 @@ -"""商品参数值模型""" -from datetime import datetime - -from sqlalchemy import Column, String, DateTime, Integer, Text - -from app.core.database import Base - - -class BasProductParameterValue(Base): - """商品参数值表""" - __tablename__ = "bas_product_parameter_value" - - id = Column(String(32), primary_key=True, comment="主键ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - parameter_id = Column(String(32), nullable=False, comment="参数ID") - parameter_name = Column(String(100), nullable=True, comment="参数名称快照") - parameter_code = Column(String(50), nullable=True, comment="参数编码快照") - param_type = Column(String(20), nullable=True, comment="参数类型快照") - param_value = Column(Text, nullable=True, comment="参数值") - sort_order = Column(Integer, default=0, comment="排序") - is_del = Column(Integer, default=0, comment="是否删除:0-否, 1-是") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BasProductParameterValueHis(Base): - """商品参数值表历史表""" - __tablename__ = "bas_product_parameter_value_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - product_id = Column(String(32), nullable=True, comment="商品ID") - parameter_id = Column(String(32), nullable=True, comment="参数ID") - parameter_name = Column(String(100), nullable=True, comment="参数名称快照") - parameter_code = Column(String(50), nullable=True, comment="参数编码快照") - param_type = Column(String(20), nullable=True, comment="参数类型快照") - param_value = Column(Text, nullable=True, comment="参数值") - sort_order = Column(Integer, default=0, comment="排序") - create_at = Column(DateTime, nullable=True, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") diff --git a/backend/app/models/product_review.py b/backend/app/models/product_review.py deleted file mode 100644 index 7e70c0f8..00000000 --- a/backend/app/models/product_review.py +++ /dev/null @@ -1,66 +0,0 @@ -"""售后评价模型""" -from datetime import datetime - -from sqlalchemy import Column, String, DateTime, Integer, Text - -from app.core.database import Base - - -class BizProductReview(Base): - """商品售后评价表""" - __tablename__ = "biz_product_review" - - id = Column(String(32), primary_key=True, comment="主键ID") - product_id = Column(String(32), nullable=False, comment="商品ID") - order_id = Column(String(32), nullable=True, comment="订单ID") - order_item_id = Column(String(32), nullable=True, comment="订单项ID") - user_id = Column(String(32), nullable=False, comment="评价用户ID") - user_name = Column(String(100), nullable=True, comment="评价用户姓名") - rating = Column(Integer, nullable=False, default=5, comment="评分(1-5星)") - rating_tags = Column(String(500), nullable=True, comment="评价标签,多个逗号分隔") - content = Column(Text, nullable=True, comment="评价内容") - is_anonymous = Column(Integer, default=0, comment="是否匿名:0-否, 1-是") - reply_content = Column(Text, nullable=True, comment="商家回复内容") - reply_time = Column(DateTime, nullable=True, comment="商家回复时间") - reply_by = Column(String(32), nullable=True, comment="回复人ID") - reply_by_name = Column(String(100), nullable=True, comment="回复人姓名") - like_count = Column(Integer, default=0, comment="点赞数") - is_top = Column(Integer, default=0, comment="是否置顶:0-否, 1-是") - status = Column(String(20), default="pending", comment="状态:pending-待审核, approved-已通过, rejected-已拒绝") - reject_reason = Column(String(500), nullable=True, comment="拒绝原因") - is_del = Column(Integer, default=0, comment="是否删除:0-否, 1-是") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizProductReviewHis(Base): - """商品售后评价历史表""" - __tablename__ = "biz_product_review_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - product_id = Column(String(32), nullable=True, comment="商品ID") - order_id = Column(String(32), nullable=True, comment="订单ID") - order_item_id = Column(String(32), nullable=True, comment="订单项ID") - user_id = Column(String(32), nullable=True, comment="评价用户ID") - user_name = Column(String(100), nullable=True, comment="评价用户姓名") - rating = Column(Integer, default=5, comment="评分") - rating_tags = Column(String(500), nullable=True, comment="评价标签") - content = Column(Text, nullable=True, comment="评价内容") - is_anonymous = Column(Integer, default=0, comment="是否匿名") - reply_content = Column(Text, nullable=True, comment="商家回复内容") - reply_time = Column(DateTime, nullable=True, comment="商家回复时间") - reply_by = Column(String(32), nullable=True, comment="回复人ID") - reply_by_name = Column(String(100), nullable=True, comment="回复人姓名") - like_count = Column(Integer, default=0, comment="点赞数") - is_top = Column(Integer, default=0, comment="是否置顶") - status = Column(String(20), default="pending", comment="状态") - reject_reason = Column(String(500), nullable=True, comment="拒绝原因") - create_at = Column(DateTime, nullable=True, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") diff --git a/backend/app/models/product_review_media.py b/backend/app/models/product_review_media.py deleted file mode 100644 index da11d992..00000000 --- a/backend/app/models/product_review_media.py +++ /dev/null @@ -1,42 +0,0 @@ -"""售后评价媒体模型""" -from datetime import datetime - -from sqlalchemy import Column, String, DateTime, Integer - -from app.core.database import Base - - -class BizProductReviewMedia(Base): - """商品评价媒体表""" - __tablename__ = "biz_product_review_media" - - id = Column(String(32), primary_key=True, comment="主键ID") - review_id = Column(String(32), nullable=False, comment="评价ID") - media_type = Column(String(20), nullable=False, default="image", comment="媒体类型:image-图片, video-视频") - file_url = Column(String(500), nullable=False, comment="文件URL地址") - thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL(视频时使用)") - sort_order = Column(Integer, default=0, comment="排序(数字越小越靠前)") - is_del = Column(Integer, default=0, comment="是否删除:0-否, 1-是") - create_at = Column(DateTime, default=datetime.now, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - - -class BizProductReviewMediaHis(Base): - """商品评价媒体历史表""" - __tablename__ = "biz_product_review_media_his" - - id = Column(String(32), primary_key=True, nullable=False, comment="主键ID") - review_id = Column(String(32), nullable=True, comment="评价ID") - media_type = Column(String(20), default="image", comment="媒体类型") - file_url = Column(String(500), nullable=True, comment="文件URL地址") - thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL") - sort_order = Column(Integer, default=0, comment="排序") - create_at = Column(DateTime, nullable=True, comment="创建时间") - create_by = Column(String(32), nullable=True, comment="创建人ID") - create_name = Column(String(100), nullable=True, comment="创建人姓名") - update_at = Column(DateTime, nullable=True, comment="修改时间") - update_by = Column(String(32), nullable=True, comment="修改人ID") - his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间") diff --git a/backend/app/services/china_region_tree.py b/backend/app/services/china_region_tree.py deleted file mode 100644 index 4f98a98b..00000000 --- a/backend/app/services/china_region_tree.py +++ /dev/null @@ -1,70 +0,0 @@ -"""中国行政区划(bas_china_region)树形结构,供运营端与小程序共用。""" -from __future__ import annotations - -import copy -import threading -import time -from collections import defaultdict -from typing import Dict, List - -from sqlalchemy.orm import Session - -from app.models.models import BasChinaRegion - -_CHINA_REGION_TREE_CACHE_TTL_SEC = 600 -_china_region_tree_cache = {"expires_at": 0.0, "tree": None} -_china_region_tree_cache_lock = threading.Lock() - - -def query_china_regions_active(db: Session) -> List[BasChinaRegion]: - return ( - db.query( - BasChinaRegion.id, - BasChinaRegion.parent_id, - BasChinaRegion.region_code, - BasChinaRegion.region_name, - BasChinaRegion.sort_order, - ) - .filter(BasChinaRegion.is_del == 0) - .order_by(BasChinaRegion.sort_order.asc(), BasChinaRegion.region_code.asc()) - .all() - ) - - -def build_china_region_tree(rows: List[BasChinaRegion]) -> List[dict]: - """{ value: region_code, label: region_name, children?: [...] },与历史 china-region-cascader.json 结构一致。""" - children_map: Dict[str, List[BasChinaRegion]] = defaultdict(list) - for r in rows: - key = getattr(r, "parent_id", None) or "" - children_map[key].append(r) - for key in children_map: - children_map[key].sort(key=lambda x: (getattr(x, "sort_order", 0), getattr(x, "region_code", ""))) - - def to_node(r: BasChinaRegion) -> dict: - rid = getattr(r, "id", None) - subs = children_map.get(rid, []) - node: dict = {"value": getattr(r, "region_code", None), "label": getattr(r, "region_name", None)} - if subs: - node["children"] = [to_node(c) for c in subs] - return node - - roots = children_map.get("", []) - return [to_node(r) for r in roots] - - -def get_china_region_tree_cached(db: Session) -> List[dict]: - """获取中国行政区划树,带进程内短 TTL 缓存。""" - now = time.time() - with _china_region_tree_cache_lock: - cache_tree = _china_region_tree_cache.get("tree") - expires_at = float(_china_region_tree_cache.get("expires_at") or 0.0) - if cache_tree is not None and now < expires_at: - return copy.deepcopy(cache_tree) - - rows = query_china_regions_active(db) - tree = build_china_region_tree(rows) - - with _china_region_tree_cache_lock: - _china_region_tree_cache["tree"] = tree - _china_region_tree_cache["expires_at"] = now + _CHINA_REGION_TREE_CACHE_TTL_SEC - return copy.deepcopy(tree) diff --git a/backend/app/services/web_auth_roles.py b/backend/app/services/web_auth_roles.py new file mode 100644 index 00000000..71225f84 --- /dev/null +++ b/backend/app/services/web_auth_roles.py @@ -0,0 +1,95 @@ +"""Web 管理端登录:基于 V0.3 sys_user / sys_user_role / sys_role 的角色与菜单权限解析""" +from __future__ import annotations + +from typing import Set + +from sqlalchemy import and_ +from sqlalchemy.orm import Session + +from app.core.config import settings +from app.models.models import SysMenu, SysRole, SysRoleMenu, SysUserRole + + +def _map_role_name_to_web(role_name: str) -> str: + """将 sys_role.role_name 映射为 web-admin 使用的 role 字符串。""" + n = (role_name or "").strip() + if not n: + return "admin" + if "超级" in n or n in ("系统管理员", "超级管理员"): + return "super_admin" + if "管理" in n or "运营" in n: + return "admin" + return "admin" + + +def resolve_web_role_for_user(db: Session, user_id: str) -> str: + """根据用户关联角色解析 Web 端 role(admin / super_admin)。""" + if not user_id: + return settings.WEB_AUTH_DEFAULT_ROLE + tenant = settings.DEFAULT_TENANT_NO + rows = ( + db.query(SysRole.role_name) + .join(SysUserRole, SysUserRole.role_id == SysRole.id) + .filter( + SysUserRole.user_id == user_id, + SysUserRole.del_flag == 0, + SysUserRole.tenant_no == tenant, + SysRole.del_flag == 0, + SysRole.status == "1", + SysRole.tenant_no == tenant, + ) + .order_by(SysRole.order_num.asc()) + .all() + ) + names = [str(r[0]).strip() for r in rows if r and r[0]] + if not names: + return settings.WEB_AUTH_DEFAULT_ROLE + mapped = [_map_role_name_to_web(n) for n in names] + if "super_admin" in mapped: + return "super_admin" + return "admin" + + +def iter_user_menu_perm_codes(db: Session, user_id: str) -> Set[str]: + """用户通过角色关联得到的菜单权限标识(sys_menu.perms,空则退回菜单 id)。""" + if not user_id: + return set() + tenant = settings.DEFAULT_TENANT_NO + try: + q = ( + db.query(SysMenu.perms, SysMenu.id) + .join( + SysRoleMenu, + and_( + SysRoleMenu.menu_id == SysMenu.id, + SysRoleMenu.del_flag == 0, + SysRoleMenu.tenant_no == tenant, + ), + ) + .join( + SysUserRole, + and_( + SysUserRole.role_id == SysRoleMenu.role_id, + SysUserRole.user_id == user_id, + SysUserRole.del_flag == 0, + SysUserRole.tenant_no == tenant, + ), + ) + .filter( + SysMenu.del_flag == 0, + SysMenu.tenant_no == tenant, + SysMenu.status == "1", + SysMenu.visible == "1", + ) + .distinct() + ) + out: Set[str] = set() + for perms, mid in q.all(): + p = (perms or "").strip() + if p: + out.add(p) + elif mid: + out.add(str(mid).strip()) + return out + except Exception: + return set() diff --git a/backend/app/tasks/__init__.py b/backend/app/tasks/__init__.py index a81e5ae2..95a43949 100644 --- a/backend/app/tasks/__init__.py +++ b/backend/app/tasks/__init__.py @@ -1,20 +1,28 @@ -"""定时任务模块(骨架:不启动叁强遗留的订单/配送定时任务)""" +"""定时任务模块(骨架:不注册业务任务)""" import logging +from apscheduler.schedulers.background import BackgroundScheduler + logger = logging.getLogger(__name__) +scheduler = BackgroundScheduler() def init_scheduled_tasks(get_db_session): - """中科兴皖骨架工程 — 不注册 APScheduler 业务任务。""" - logger.info("init_scheduled_tasks: 已跳过业务定时任务(骨架模式)") + """初始化定时任务(骨架工程无默认任务)""" + if scheduler.running: + return + scheduler.start() + logger.info("定时任务调度器已启动(骨架模式:无业务 Job)") def shutdown_scheduler(): - """无调度器实例时为空操作。""" - pass + if scheduler.running: + scheduler.shutdown(wait=False) + logger.info("定时任务调度器已关闭") __all__ = [ + "scheduler", "init_scheduled_tasks", "shutdown_scheduler", ] diff --git a/backend/app/tasks/delivery_warning.py b/backend/app/tasks/delivery_warning.py deleted file mode 100644 index e4f7125d..00000000 --- a/backend/app/tasks/delivery_warning.py +++ /dev/null @@ -1,236 +0,0 @@ -"""配送时效预警定时任务 - -依据订单分配时写入的配送截止时间(biz_delivery_task.effective_time)及区域表 sys_region.delivery_warn_advance_minutes(缺省 30 分钟)生成运营端预警: - -- 进入「截止时间前 N 分钟」窗口且仍未完成配送:生成一条 advance_deadline 预警; -- 已超过截止时间仍未完成:生成 timeout 预警; -- 配送员确认送达(任务状态 delivering)或任务取消/完成时,由业务侧自动结案预警。 - -建议:每 1~5 分钟执行一次 check_delivery_warnings(db)。 -""" -import logging -from datetime import datetime -from typing import Optional - -from sqlalchemy.orm import Session - -from app.models.models import BizDeliveryTask, BizDeliveryWarning, SysRegion -from app.core.utils import generate_id -from app.crud.delivery_warning_resolve import auto_resolve_open_warnings_for_task - -logger = logging.getLogger(__name__) - -DEFAULT_ADVANCE_MINUTES = 30 - - -class DeliveryWarningTask: - """配送时效预警任务""" - - def __init__(self, db: Session): - self.db = db - - def check_delivery_warnings(self) -> dict: - """检查配送任务并生成预警""" - result = { - "checked": 0, - "warnings_created": 0, - "notifications_sent": 0, - "errors": [] - } - - try: - tasks = ( - self.db.query(BizDeliveryTask) - .filter(BizDeliveryTask.is_del == 0) - .all() - ) - result["checked"] = len(tasks) - - for task in tasks: - try: - if task.status in ("delivering", "completed", "cancelled"): - auto_resolve_open_warnings_for_task(self.db, task.id) - continue - - if task.status not in ("pending", "assigned", "picking"): - continue - - warnings = self._check_task_warnings(task) - result["warnings_created"] += len(warnings) - - for warning in warnings: - self._send_warning_notification(warning) - result["notifications_sent"] += 1 - - except Exception as e: - logger.error(f"处理任务 {task.id} 时出错: {str(e)}") - result["errors"].append({ - "task_id": task.id, - "error": str(e) - }) - - self.db.commit() - - except Exception as e: - logger.error(f"配送时效预警任务执行失败: {str(e)}") - self.db.rollback() - result["errors"].append({"task_id": "all", "error": str(e)}) - - return result - - def _region_advance_minutes(self, region_id: str) -> int: - reg = self.db.query(SysRegion).filter(SysRegion.id == region_id, SysRegion.is_del == 0).first() - if not reg: - return DEFAULT_ADVANCE_MINUTES - m = getattr(reg, "delivery_warn_advance_minutes", None) - if m is None or int(m) < 1: - return DEFAULT_ADVANCE_MINUTES - return int(m) - - def _check_task_warnings(self, task: BizDeliveryTask) -> list: - warnings = [] - - if not task.effective_time: - return warnings - - now = datetime.now() - remaining_minutes = int((task.effective_time - now).total_seconds() // 60) - advance = self._region_advance_minutes(task.region_id) - - warning_type = None - urgency_level = None - message = None - if remaining_minutes < 0: - warning_type = "timeout" - urgency_level = "已超时" - message = f"已超过配送时效 {abs(remaining_minutes)} 分钟,仍未完成配送" - elif remaining_minutes <= advance: - warning_type = "advance_deadline" - urgency_level = "时效预警" - message = f"距配送截止不足 {advance} 分钟(剩余约 {remaining_minutes} 分钟),请督促配送" - else: - return warnings - - w = self._upsert_task_warning( - task=task, - warning_type=warning_type, - urgency_level=urgency_level, - remaining_minutes=remaining_minutes, - message=message, - ) - if w: - warnings.append(w) - - return warnings - - def _upsert_task_warning( - self, - task: BizDeliveryTask, - warning_type: str, - urgency_level: str, - remaining_minutes: int, - message: str - ) -> Optional[BizDeliveryWarning]: - """单任务单预警:存在则更新,不存在则创建""" - try: - warning = self._get_latest_warning_by_task(task.id) - now = datetime.now() - if warning: - warning.warning_type = warning_type - warning.urgency_level = urgency_level - warning.remaining_minutes = remaining_minutes - warning.message = message - warning.order_id = task.order_id - warning.is_sent = 0 - # 未完成前统一保持未结案(若历史上被手工结案,则重新激活) - warning.is_resolved = 0 - warning.resolved_time = None - warning.resolved_by = None - warning.update_at = now - warning.update_by = "system" - self.db.flush() - return warning - - warning = BizDeliveryWarning( - id=generate_id(), - task_id=task.id, - order_id=task.order_id, - warning_type=warning_type, - urgency_level=urgency_level, - remaining_minutes=remaining_minutes, - message=message, - is_sent=0, - is_resolved=0, - create_by="system", - create_name="系统", - ) - self.db.add(warning) - self.db.flush() - return warning - except Exception as e: - logger.error(f"创建预警记录失败: {str(e)}") - return None - - def _get_latest_warning_by_task(self, task_id: str) -> Optional[BizDeliveryWarning]: - return self.db.query(BizDeliveryWarning).filter( - BizDeliveryWarning.task_id == task_id, - BizDeliveryWarning.is_del == 0, - ).order_by(BizDeliveryWarning.update_at.desc(), BizDeliveryWarning.create_at.desc()).first() - - def _send_warning_notification(self, warning: BizDeliveryWarning) -> bool: - """发送预警通知(可通过微信模板消息等)""" - try: - # 获取配送员信息 - task = self.db.query(BizDeliveryTask).filter( - BizDeliveryTask.id == warning.task_id - ).first() - - if not task or not task.deliver_id: - logger.warning(f"任务 {warning.task_id} 未分配配送员,跳过通知") - return False - - # TODO: 调用微信模板消息接口发送通知 - # from app.core.wechat_pay import send_delivery_warning_message - # send_delivery_warning_message( - # deliver_id=task.deliver_id, - # task_no=task.task_no, - # message=warning.message - # ) - - logger.info(f"已发送预警通知: {task.task_no} - {warning.message}") - return True - - except Exception as e: - logger.error(f"发送预警通知失败: {str(e)}") - return False - - -def check_delivery_warnings(db: Session) -> dict: - """执行配送时效预警检查(供外部调用)""" - task = DeliveryWarningTask(db) - return task.check_delivery_warnings() - - -def cleanup_expired_warnings(db: Session, days: int = 7) -> int: - """清理过期的预警记录 - - Args: - db: 数据库会话 - days: 保留天数,默认7天 - - Returns: - int: 删除的记录数 - """ - from datetime import timedelta - - cutoff_date = datetime.now() - timedelta(days=days) - - count = db.query(BizDeliveryWarning).filter( - BizDeliveryWarning.create_at < cutoff_date, - BizDeliveryWarning.is_resolved == 1, - BizDeliveryWarning.is_del == 0 - ).update({"is_del": 1}) - - db.commit() - logger.info(f"已清理 {count} 条过期预警记录") - return count diff --git a/backend/app/tasks/order_auto_confirm_receive.py b/backend/app/tasks/order_auto_confirm_receive.py deleted file mode 100644 index f796dfc2..00000000 --- a/backend/app/tasks/order_auto_confirm_receive.py +++ /dev/null @@ -1,28 +0,0 @@ -"""送达后超时未确认收货,系统自动确认。""" -import logging -from datetime import datetime, timedelta - -from sqlalchemy.orm import Session - -from app.crud.order import order_crud - -logger = logging.getLogger(__name__) - - -def run_auto_confirm_receive_overdue(db: Session) -> int: - from app.api.system_config import get_auto_confirm_receive_days_for_job - - days = get_auto_confirm_receive_days_for_job(db) - before = datetime.now() - timedelta(days=days) - count = order_crud.auto_confirm_receive_overdue( - db, - before_threshold=before, - configured_days=days, - ) - return count - - -def run_auto_confirm_receive_job(db: Session): - count = run_auto_confirm_receive_overdue(db) - if count: - logger.info(f"自动确认收货完成: {count} 条") diff --git a/backend/app/tasks/order_payment_timeout.py b/backend/app/tasks/order_payment_timeout.py deleted file mode 100644 index 152a748c..00000000 --- a/backend/app/tasks/order_payment_timeout.py +++ /dev/null @@ -1,27 +0,0 @@ -"""订单支付超时自动关闭任务。""" -import logging -from datetime import datetime, timedelta - -from sqlalchemy.orm import Session - -from app.crud.order import order_crud - -logger = logging.getLogger(__name__) - - -def close_timeout_online_orders(db: Session) -> int: - """关闭超时未支付的线上订单(超时分钟数来自系统设置 auto_cancel_minutes)。""" - from app.api.system_config import get_auto_cancel_minutes_for_job - - timeout_minutes = get_auto_cancel_minutes_for_job(db) - before_time = datetime.now() - timedelta(minutes=timeout_minutes) - timeout_note = f"超过未支付自动取消时限({timeout_minutes} 分钟)" - return order_crud.close_unpaid_online_orders( - db, before_time=before_time, timeout_note=timeout_note - ) - - -def run_close_timeout_online_orders(db: Session): - """定时任务包装。""" - count = close_timeout_online_orders(db) - logger.info(f"线上支付超时订单自动关闭完成: {count} 条") diff --git a/backend/app/ws/price_updates.py b/backend/app/ws/price_updates.py deleted file mode 100644 index 85ecb911..00000000 --- a/backend/app/ws/price_updates.py +++ /dev/null @@ -1,187 +0,0 @@ -"""WebSocket:区域价格清单更新推送(客户端实时刷新)""" - -from __future__ import annotations - -import asyncio -import logging -from dataclasses import dataclass -from typing import Dict, Set, Optional, Any - -import anyio -from fastapi import WebSocket, WebSocketDisconnect - -from app.core.database import SessionLocal -from app.core.auth import get_current_user_by_token - -logger = logging.getLogger(__name__) - -@dataclass(frozen=True) -class PriceListUpdatedEvent: - regionId: str - priceListId: str - priceVersion: int - publishedTime: Optional[str] = None - - def to_message(self) -> dict: - return { - "event": "price_list_updated", - "data": { - "regionId": self.regionId, - "priceListId": self.priceListId, - "priceVersion": self.priceVersion, - "publishedTime": self.publishedTime, - }, - } - - -class RegionWSManager: - """按 region_id 管理 websocket 连接并广播。""" - - def __init__(self) -> None: - self._lock = asyncio.Lock() - self._rooms: Dict[str, Set[WebSocket]] = {} - - async def connect(self, websocket: WebSocket, *, token: str) -> dict: - """鉴权并接入房间。返回 current_user dict。""" - # 验证 token(包含 Redis 会话校验 + DB 用户校验) - db = SessionLocal() - try: - current_user = await get_current_user_by_token(token, db) - finally: - db.close() - - region_id = (current_user.get("region_id") or "").strip() - if not region_id: - logger.warning("[ws-mini] reject: empty region_id user=%s", current_user.get("id")) - await websocket.close(code=1008) - raise RuntimeError("user has no region_id") - - await websocket.accept() - - async with self._lock: - self._rooms.setdefault(region_id, set()).add(websocket) - room_size = len(self._rooms.get(region_id, set())) - logger.warning( - "[ws-mini] connected user=%s role=%s region=%s room_size=%s", - current_user.get("id"), - current_user.get("role"), - region_id, - room_size, - ) - - return current_user - - async def disconnect(self, websocket: WebSocket, *, region_id: str) -> None: - async with self._lock: - s = self._rooms.get(region_id) - if not s: - return - if websocket in s: - s.remove(websocket) - room_size = len(s) - if not s: - self._rooms.pop(region_id, None) - logger.warning("[ws-mini] disconnected region=%s room_size=%s", region_id, room_size if "room_size" in locals() else 0) - - async def broadcast_region(self, region_id: str, message: dict) -> int: - """向 region 房间广播消息。返回尝试发送的连接数。""" - async with self._lock: - targets = list(self._rooms.get(region_id, set())) - - if not targets: - logger.warning("[ws-mini] broadcast skip: no targets region=%s event=%s", region_id, message.get("event")) - return 0 - - logger.warning( - "[ws-mini] broadcasting region=%s event=%s target_count=%s", - region_id, - message.get("event"), - len(targets), - ) - dead: list[WebSocket] = [] - for ws in targets: - try: - await ws.send_json(message) - except Exception: - dead.append(ws) - - if dead: - async with self._lock: - room = self._rooms.get(region_id) - if room: - for ws in dead: - room.discard(ws) - if not room: - self._rooms.pop(region_id, None) - - return len(targets) - - -manager = RegionWSManager() - - -async def ws_mini_price_updates(websocket: WebSocket) -> None: - """小程序端 WebSocket 入口:/ws/mini""" - token = (websocket.query_params.get("token") or "").strip() - if not token: - # 兼容 Authorization: Bearer - auth = (websocket.headers.get("authorization") or "").strip() - if auth.lower().startswith("bearer "): - token = auth[7:].strip() - - if not token: - logger.warning("[ws-mini] reject: missing token ip=%s", websocket.client.host if websocket.client else "") - await websocket.close(code=1008) - return - - try: - current_user = await manager.connect(websocket, token=token) - except Exception: - logger.exception("[ws-mini] connect failed") - # connect 内部会 close;这里兜底 - try: - await websocket.close(code=1008) - except Exception: - pass - return - - region_id = (current_user.get("region_id") or "").strip() - try: - # 当前实现:仅服务器推送;客户端无需主动发消息。 - while True: - await websocket.receive_text() - except WebSocketDisconnect: - logger.warning("[ws-mini] client disconnected user=%s region=%s", current_user.get("id"), region_id) - pass - except Exception: - logger.exception("[ws-mini] receive loop error user=%s region=%s", current_user.get("id"), region_id) - pass - finally: - await manager.disconnect(websocket, region_id=region_id) - - -def notify_price_list_updated(event: PriceListUpdatedEvent) -> None: - """线程/同步上下文安全:广播价格更新事件。""" - - async def _run(): - sent = await manager.broadcast_region(event.regionId, event.to_message()) - logger.warning( - "[ws-mini] notify price update region=%s price_list_id=%s version=%s sent=%s", - event.regionId, - event.priceListId, - event.priceVersion, - sent, - ) - - try: - loop = asyncio.get_running_loop() - loop.create_task(_run()) - except RuntimeError: - # sync endpoint 常在 worker thread:使用 anyio 跳回事件循环 - anyio.from_thread.run(_run) - - -def register_ws_routes(app: Any) -> None: - """在 FastAPI app 上注册 websocket 路由。""" - app.websocket("/ws/mini")(ws_mini_price_updates) - diff --git a/docs/sql/zkxw-requirements-v0.3-schema.sql b/docs/sql/zkxw-requirements-v0.3-schema.sql new file mode 100644 index 00000000..c9df5f6e --- /dev/null +++ b/docs/sql/zkxw-requirements-v0.3-schema.sql @@ -0,0 +1,1638 @@ +-- ============================================================================= +-- 中科兴皖需求 V0.3 表模型 DDL(MySQL 5.7) +-- 依据:docs/中科兴皖需求V0.3.md 第 2、3 节 +-- 说明: +-- 1) id 为 VARCHAR(64),由应用生成 UUID;未启用外键约束,便于分步执行与迁移。 +-- 2) 公共字段顺序:业务字段 → tenant_no → order_num → remark → create_* → update_* → del_flag +-- 3) 文档标注「暂不开发」的模块(科研捐赠、临床应用、物料、财务、生产、质检)未建表。 +-- ============================================================================= + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ----------------------------------------------------------------------------- +-- 公共列片段(复制到各表;此处仅作文档说明,实际已展开写入各 CREATE TABLE) +-- tenant_no, order_num, remark, create_by, create_dept_id, create_time, +-- update_by, update_time, del_flag +-- ----------------------------------------------------------------------------- + +-- ========== 2.1.1 租户管理 ========== +DROP TABLE IF EXISTS `sys_tenant`; +CREATE TABLE `sys_tenant` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `tenant_no` VARCHAR(64) NOT NULL COMMENT '租户编号(业务编码,如 zkxw)', + `tenant_name` VARCHAR(200) NOT NULL COMMENT '租户名称', + `contact_name` VARCHAR(100) DEFAULT NULL COMMENT '联系人姓名', + `contact_phone` VARCHAR(32) DEFAULT NULL COMMENT '联系电话', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_sys_tenant_no` (`tenant_no`, `del_flag`), + KEY `idx_sys_tenant_del` (`del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户信息'; + +-- ========== 2.1.2 字典管理 ========== +DROP TABLE IF EXISTS `sys_dict_type`; +CREATE TABLE `sys_dict_type` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `dict_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典名称', + `dict_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典类型编码', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_dict_type` (`tenant_no`, `dict_type`, `del_flag`), + KEY `idx_dict_type_tenant` (`tenant_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='字典类型'; + +DROP TABLE IF EXISTS `sys_dict_data`; +CREATE TABLE `sys_dict_data` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `dict_label` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典标签', + `dict_value` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典键值', + `dict_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典类型', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_dict_data_type` (`tenant_no`, `dict_type`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='字典数据'; + +-- ========== 2.1.3 组织管理 ========== +DROP TABLE IF EXISTS `sys_dept`; +CREATE TABLE `sys_dept` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `parent_id` VARCHAR(64) NOT NULL DEFAULT '0' COMMENT '父组织id,0为根', + `dept_name` VARCHAR(200) NOT NULL COMMENT '组织名称', + `dept_category_code` VARCHAR(64) DEFAULT NULL COMMENT '组织类别编码(字典 zzlbbm)', + `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '部门状态(字典 bmzt:0停用 1正常)', + `leader` VARCHAR(100) DEFAULT NULL COMMENT '部门负责人', + `leader_phone` VARCHAR(32) DEFAULT NULL COMMENT '部门负责人联系电话', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_dept_parent` (`parent_id`, `del_flag`), + KEY `idx_dept_tenant` (`tenant_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织/部门'; + +-- ========== 2.1.4 用户管理 ========== +DROP TABLE IF EXISTS `sys_user`; +CREATE TABLE `sys_user` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `dept_id` VARCHAR(64) DEFAULT NULL COMMENT '部门ID', + `user_no` VARCHAR(30) NOT NULL COMMENT '用户账号', + `nick_name` VARCHAR(30) NOT NULL COMMENT '用户名', + `email` VARCHAR(50) DEFAULT '' COMMENT '用户邮箱', + `phone_no` VARCHAR(11) DEFAULT '' COMMENT '手机号码', + `sex` CHAR(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)', + `avatar` VARCHAR(512) DEFAULT NULL COMMENT '头像(minio完整地址)', + `password` VARCHAR(100) DEFAULT '' COMMENT '密码', + `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '帐号状态(1正常 0停用)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_no` (`tenant_no`, `user_no`, `del_flag`), + KEY `idx_user_dept` (`dept_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户'; + +-- ========== 2.1.5 角色管理 ========== +DROP TABLE IF EXISTS `sys_role`; +CREATE TABLE `sys_role` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `role_name` VARCHAR(30) NOT NULL COMMENT '角色名称', + `data_scope` CHAR(1) DEFAULT '1' COMMENT '数据权限(1全部 2本部门 3本部门及以下 4仅本人 5自定义)', + `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '角色状态(1正常 0停用)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_role_tenant` (`tenant_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色'; + +DROP TABLE IF EXISTS `sys_user_role`; +CREATE TABLE `sys_user_role` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID', + `role_id` VARCHAR(64) NOT NULL COMMENT '角色ID', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_role` (`user_id`, `role_id`, `del_flag`), + KEY `idx_user_role_role` (`role_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户-角色关联'; + +DROP TABLE IF EXISTS `sys_role_menu`; +CREATE TABLE `sys_role_menu` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `role_id` VARCHAR(64) NOT NULL COMMENT '角色ID', + `menu_id` VARCHAR(64) NOT NULL COMMENT '菜单ID', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`, `del_flag`), + KEY `idx_role_menu_menu` (`menu_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-菜单关联'; + +DROP TABLE IF EXISTS `sys_role_data_scope_detail`; +CREATE TABLE `sys_role_data_scope_detail` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `role_id` VARCHAR(64) NOT NULL COMMENT '角色ID', + `scope_type` VARCHAR(20) NOT NULL COMMENT '范围类型:dept部门 user用户', + `scope_value` VARCHAR(100) NOT NULL COMMENT '范围值:部门ID/用户ID', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_role_scope_role` (`role_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色自定义数据权限明细'; + +-- ========== 2.1.6 菜单管理 ========== +DROP TABLE IF EXISTS `sys_menu`; +CREATE TABLE `sys_menu` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `menu_name` VARCHAR(50) NOT NULL COMMENT '菜单名称', + `parent_id` VARCHAR(64) NOT NULL DEFAULT '0' COMMENT '父菜单ID,0为根', + `menu_type` CHAR(1) DEFAULT 'M' COMMENT '类型 M目录 C菜单 F按钮(扩展)', + `path` VARCHAR(200) DEFAULT '' COMMENT '路由路径(扩展)', + `component` VARCHAR(255) DEFAULT NULL COMMENT '组件路径(扩展)', + `perms` VARCHAR(100) DEFAULT NULL COMMENT '权限标识(扩展)', + `is_frame` INT(1) DEFAULT 1 COMMENT '是否外链(1是 0否)', + `visible` CHAR(1) DEFAULT '1' COMMENT '显示状态(1显示 0隐藏)', + `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '菜单状态(1正常 0停用)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_menu_parent` (`parent_id`, `del_flag`), + KEY `idx_menu_tenant` (`tenant_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单'; + +-- ========== 2.1.7 站点管理 ========== +DROP TABLE IF EXISTS `sys_site`; +CREATE TABLE `sys_site` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `site_name` VARCHAR(200) NOT NULL COMMENT '站点名称', + `site_code` VARCHAR(64) NOT NULL COMMENT '站点编码', + `site_type` VARCHAR(64) DEFAULT NULL COMMENT '站点类型(字典 zdlx)', + `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '状态(1正常 0停用)', + `address` VARCHAR(512) DEFAULT NULL COMMENT '详细地址', + `contact_name` VARCHAR(100) DEFAULT NULL COMMENT '联系人', + `contact_phone` VARCHAR(32) DEFAULT NULL COMMENT '联系电话', + `business_scope` VARCHAR(256) DEFAULT NULL COMMENT '业务范围(字典 zdywfw,多值可逗号分隔)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_site_code` (`tenant_no`, `site_code`, `del_flag`), + KEY `idx_site_tenant` (`tenant_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点'; + +-- ========== 2.1.8 职位管理 ========== +DROP TABLE IF EXISTS `sys_post`; +CREATE TABLE `sys_post` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `post_name` VARCHAR(100) NOT NULL COMMENT '职位名称', + `post_code` VARCHAR(64) NOT NULL COMMENT '职位编码', + `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '状态(1正常 0停用)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_post_code` (`tenant_no`, `post_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='职位'; + +DROP TABLE IF EXISTS `sys_user_post`; +CREATE TABLE `sys_user_post` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID', + `post_id` VARCHAR(64) NOT NULL COMMENT '职位ID', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_post` (`user_id`, `post_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户-职位关联'; + +-- ========== 2.1.9 物品单位 ========== +DROP TABLE IF EXISTS `biz_item_unit`; +CREATE TABLE `biz_item_unit` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `unit_code` VARCHAR(64) NOT NULL COMMENT '单位编码', + `unit_name` VARCHAR(100) NOT NULL COMMENT '单位名称', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_item_unit` (`tenant_no`, `unit_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='物品单位'; + +-- ========== 2.1.10 快递公司 ========== +DROP TABLE IF EXISTS `biz_express_company`; +CREATE TABLE `biz_express_company` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `express_code` VARCHAR(64) NOT NULL COMMENT '快递公司编码', + `express_name` VARCHAR(200) NOT NULL COMMENT '快递公司名称', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_express` (`tenant_no`, `express_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='快递公司'; + +-- ========== 2.1.11 工作流(概要模型,可随实现扩展) ========== +DROP TABLE IF EXISTS `wf_process_def`; +CREATE TABLE `wf_process_def` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `process_code` VARCHAR(64) NOT NULL COMMENT '流程编码', + `process_name` VARCHAR(200) NOT NULL COMMENT '流程名称', + `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '1启用 0停用', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_wf_def` (`tenant_no`, `process_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='流程定义'; + +DROP TABLE IF EXISTS `wf_process_version`; +CREATE TABLE `wf_process_version` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `process_def_id` VARCHAR(64) NOT NULL COMMENT '流程定义ID', + `version_no` INT NOT NULL COMMENT '版本号,递增', + `graph_json` MEDIUMTEXT COMMENT '节点与连线等配置(JSON 字符串)', + `is_latest` CHAR(1) DEFAULT '0' COMMENT '是否最新版本(1是 0否)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_wf_ver_def` (`process_def_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='流程定义版本'; + +DROP TABLE IF EXISTS `wf_node`; +CREATE TABLE `wf_node` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `process_version_id` VARCHAR(64) NOT NULL COMMENT '流程版本ID', + `node_key` VARCHAR(64) NOT NULL COMMENT '节点唯一键(画布内)', + `node_type` VARCHAR(32) NOT NULL COMMENT 'start/end/approval/condition 等', + `node_name` VARCHAR(200) DEFAULT NULL COMMENT '节点名称', + `config_json` MEDIUMTEXT COMMENT '审批人/SQL/会签策略等配置', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_wf_node_ver` (`process_version_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='流程节点'; + +DROP TABLE IF EXISTS `wf_transition`; +CREATE TABLE `wf_transition` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `process_version_id` VARCHAR(64) NOT NULL COMMENT '流程版本ID', + `from_node_key` VARCHAR(64) NOT NULL COMMENT '起始节点key', + `to_node_key` VARCHAR(64) NOT NULL COMMENT '目标节点key', + `condition_expr` VARCHAR(512) DEFAULT NULL COMMENT '条件表达式或分支说明', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_wf_trans_ver` (`process_version_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='流程连线/转移'; + +DROP TABLE IF EXISTS `wf_process_instance`; +CREATE TABLE `wf_process_instance` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `process_version_id` VARCHAR(64) NOT NULL COMMENT '发起时锁定的流程版本ID', + `business_type` VARCHAR(64) DEFAULT NULL COMMENT '业务类型', + `business_id` VARCHAR(64) DEFAULT NULL COMMENT '业务主键', + `starter_user_id` VARCHAR(64) DEFAULT NULL COMMENT '发起人', + `variables_json` MEDIUMTEXT COMMENT '流程变量 JSON', + `status` VARCHAR(32) DEFAULT NULL COMMENT 'running/completed/rejected 等', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_wf_inst_biz` (`business_type`, `business_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='流程实例'; + +DROP TABLE IF EXISTS `wf_task`; +CREATE TABLE `wf_task` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `instance_id` VARCHAR(64) NOT NULL COMMENT '流程实例ID', + `node_key` VARCHAR(64) DEFAULT NULL COMMENT '当前节点key', + `assignee_user_id` VARCHAR(64) DEFAULT NULL COMMENT '待办人', + `status` VARCHAR(32) DEFAULT NULL COMMENT 'pending/approved/rejected 等', + `comment_text` VARCHAR(512) DEFAULT NULL COMMENT '审批意见', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_wf_task_inst` (`instance_id`, `del_flag`), + KEY `idx_wf_task_user` (`assignee_user_id`, `status`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='流程任务/待办'; + +-- ========== 2.2.1 客户管理 customer ========== +DROP TABLE IF EXISTS `biz_customer`; +CREATE TABLE `biz_customer` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `customer_name` VARCHAR(100) NOT NULL COMMENT '客户姓名', + `sex` CHAR(1) DEFAULT '2' COMMENT '性别(0男 1女 2未知)', + `id_card_no` VARCHAR(32) DEFAULT NULL COMMENT '身份证号', + `age` INT DEFAULT NULL COMMENT '年龄', + `phone_no` VARCHAR(32) DEFAULT NULL COMMENT '联系电话', + `customer_tags` VARCHAR(512) DEFAULT NULL COMMENT '客户标签,逗号分隔', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_customer_idcard` (`tenant_no`, `id_card_no`, `del_flag`), + KEY `idx_customer_name` (`tenant_no`, `customer_name`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户信息'; + +DROP TABLE IF EXISTS `biz_customer_follow`; +CREATE TABLE `biz_customer_follow` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `customer_id` VARCHAR(64) NOT NULL COMMENT '客户ID', + `follow_time` DATETIME NOT NULL COMMENT '随访时间', + `follow_desc` VARCHAR(2000) DEFAULT NULL COMMENT '随访描述', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_follow_customer` (`customer_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户随访'; + +-- ========== 2.2.2 细胞样本类型 ========== +DROP TABLE IF EXISTS `biz_cell_sample_type`; +CREATE TABLE `biz_cell_sample_type` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `type_code` VARCHAR(64) NOT NULL COMMENT '样本类型编码', + `type_name` VARCHAR(200) NOT NULL COMMENT '样本类型名称', + `storage_years` INT DEFAULT NULL COMMENT '样本存储年份', + `storage_price` DECIMAL(18,2) DEFAULT NULL COMMENT '样本存储价格(元)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_sample_type` (`tenant_no`, `type_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='细胞样本类型'; + +-- ========== 2.2.3 采集地区及医院(1 地区 : N 医院) ========== +DROP TABLE IF EXISTS `biz_collection_hospital`; +DROP TABLE IF EXISTS `biz_collection_region`; +CREATE TABLE `biz_collection_region` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `region_name` VARCHAR(200) NOT NULL COMMENT '地区名称', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_region_tenant` (`tenant_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集地区'; + +CREATE TABLE `biz_collection_hospital` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `region_id` VARCHAR(64) NOT NULL COMMENT '采集地区ID', + `hospital_name` VARCHAR(200) NOT NULL COMMENT '医院名称', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_hospital_region` (`region_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集地区下属医院'; + +-- ========== 2.2.4 采集协议号 ========== +DROP TABLE IF EXISTS `biz_collection_protocol_no`; +CREATE TABLE `biz_collection_protocol_no` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `protocol_no` VARCHAR(32) NOT NULL COMMENT '采集协议号', + `issued` TINYINT NOT NULL DEFAULT 0 COMMENT '是否下发(1是 0否)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_protocol_no` (`tenant_no`, `protocol_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议号池'; + +-- ========== 2.2.5 样本编码 ========== +DROP TABLE IF EXISTS `biz_sample_code`; +CREATE TABLE `biz_sample_code` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `sample_code` VARCHAR(32) NOT NULL COMMENT '样本编码', + `issued` TINYINT NOT NULL DEFAULT 0 COMMENT '是否下发(1是 0否)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_sample_code` (`tenant_no`, `sample_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='样本编码池'; + +-- ========== 2.2.6 采集协议单及子表 ========== +DROP TABLE IF EXISTS `biz_protocol_operation_log`; +DROP TABLE IF EXISTS `biz_protocol_storage_cert`; +DROP TABLE IF EXISTS `biz_protocol_fullmoon_todo`; +DROP TABLE IF EXISTS `biz_protocol_audit`; +DROP TABLE IF EXISTS `biz_protocol_party_customer`; +DROP TABLE IF EXISTS `biz_protocol_payment_detail`; +DROP TABLE IF EXISTS `biz_protocol_payment`; +DROP TABLE IF EXISTS `biz_protocol_sample_transport`; +DROP TABLE IF EXISTS `biz_protocol_sample`; +DROP TABLE IF EXISTS `biz_protocol_collection_info`; +DROP TABLE IF EXISTS `biz_collection_protocol`; + +CREATE TABLE `biz_collection_protocol` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `protocol_no_id` VARCHAR(64) DEFAULT NULL COMMENT '采集协议号表ID(选用号段)', + `protocol_no` VARCHAR(32) NOT NULL COMMENT '采集协议号展示', + `protocol_status` VARCHAR(16) NOT NULL DEFAULT 'dsx' COMMENT 'dsx待生效 sxz生效中 ygq已过期 yzf已作废 zky转科研', + `protocol_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '协议附件 minio 地址逗号分隔', + `register_date` CHAR(8) DEFAULT NULL COMMENT '登记日期YYYYMMDD', + `effective_time` DATETIME DEFAULT NULL COMMENT '协议生效时间', + `effective_desc` VARCHAR(512) DEFAULT NULL COMMENT '协议生效描述', + `expire_time` DATETIME DEFAULT NULL COMMENT '协议过期时间', + `expire_desc` VARCHAR(512) DEFAULT NULL COMMENT '协议过期描述', + `void_time` DATETIME DEFAULT NULL COMMENT '协议作废时间', + `void_desc` VARCHAR(512) DEFAULT NULL COMMENT '协议作废描述', + `to_research_time` DATETIME DEFAULT NULL COMMENT '协议转科研时间', + `to_research_desc` VARCHAR(512) DEFAULT NULL COMMENT '协议转科研描述', + `collector_user_id` VARCHAR(64) DEFAULT NULL COMMENT '采集专员用户ID', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注信息', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_cp_status` (`tenant_no`, `protocol_status`, `del_flag`), + KEY `idx_cp_no` (`tenant_no`, `protocol_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单'; + +CREATE TABLE `biz_protocol_collection_info` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `collection_protocol_id` VARCHAR(64) NOT NULL COMMENT '采集协议单ID', + `region_id` VARCHAR(64) DEFAULT NULL COMMENT '采集地区ID', + `hospital_id` VARCHAR(64) DEFAULT NULL COMMENT '采集医院ID', + `medical_staff` VARCHAR(200) DEFAULT NULL COMMENT '采集医护', + `sales_user_id` VARCHAR(64) DEFAULT NULL COMMENT '销售人员用户ID', + `sample_collect_time` DATETIME DEFAULT NULL COMMENT '样本采集时间', + `maternal_blood_collect_time` DATETIME DEFAULT NULL COMMENT '母血采集时间', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_coll_one` (`collection_protocol_id`, `del_flag`), + KEY `idx_coll_cp` (`collection_protocol_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-采集信息'; + +CREATE TABLE `biz_protocol_sample` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `collection_protocol_id` VARCHAR(64) NOT NULL COMMENT '采集协议单ID', + `sample_type_id` VARCHAR(64) DEFAULT NULL COMMENT '样本类型ID', + `sample_code_id` VARCHAR(64) DEFAULT NULL COMMENT '样本编码表ID', + `sample_code` VARCHAR(32) DEFAULT NULL COMMENT '样本编码展示', + `collect_amount` DECIMAL(18,4) DEFAULT NULL COMMENT '采集量', + `unit_id` VARCHAR(64) DEFAULT NULL COMMENT '物品单位ID', + `price` DECIMAL(18,2) DEFAULT NULL COMMENT '价格', + `storage_years` INT DEFAULT NULL COMMENT '存储年限(年)', + `sample_status` VARCHAR(16) NOT NULL DEFAULT 'ycj' COMMENT 'ycj已采集 jcz检测中 yrk已入库 yzf已作废', + `storage_in_date` CHAR(8) DEFAULT NULL COMMENT '入库日期YYYYMMDD', + `detect_start_date` CHAR(8) DEFAULT NULL COMMENT '检测开始日期', + `void_date` CHAR(8) DEFAULT NULL COMMENT '作废日期', + `storage_expire_date` CHAR(8) DEFAULT NULL COMMENT '存储到期日期=入库+年限', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_sample_cp` (`collection_protocol_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-样本信息'; + +CREATE TABLE `biz_protocol_sample_transport` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `collection_protocol_id` VARCHAR(64) NOT NULL COMMENT '采集协议单ID', + `sample_id` VARCHAR(64) NOT NULL COMMENT '样本ID', + `transport_type` VARCHAR(16) NOT NULL COMMENT 'kdys快递运输 ryxd人员携带', + `express_company_id` VARCHAR(64) DEFAULT NULL COMMENT '快递公司ID', + `waybill_no` VARCHAR(128) DEFAULT NULL COMMENT '快递单号', + `dest_site_id` VARCHAR(64) DEFAULT NULL COMMENT '目的站点ID', + `express_fee` DECIMAL(18,2) DEFAULT NULL COMMENT '快递费用(元)', + `eta_time` DATETIME DEFAULT NULL COMMENT '预计到达时间', + `carrier_name` VARCHAR(100) DEFAULT NULL COMMENT '携带人员姓名', + `carrier_phone` VARCHAR(32) DEFAULT NULL COMMENT '携带人员联系电话', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_transport_sample` (`sample_id`, `del_flag`), + KEY `idx_transport_cp` (`collection_protocol_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-样本运输'; + +CREATE TABLE `biz_protocol_payment` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `collection_protocol_id` VARCHAR(64) NOT NULL COMMENT '采集协议单ID', + `pay_mode` VARCHAR(8) NOT NULL COMMENT 'dj趸交 fq分期', + `contract_amount` DECIMAL(18,2) DEFAULT NULL COMMENT '合同金额', + `installment_count` INT DEFAULT NULL COMMENT '分期期数', + `installment_interval_months` INT DEFAULT NULL COMMENT '每期间隔(月)', + `lump_sum_deadline` CHAR(8) DEFAULT NULL COMMENT '趸交截止时间YYYYMMDD', + `installment_start_date` CHAR(8) DEFAULT NULL COMMENT '分期开始时间YYYYMMDD', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_payment_cp` (`collection_protocol_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-缴费主表'; + +CREATE TABLE `biz_protocol_payment_detail` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `payment_id` VARCHAR(64) NOT NULL COMMENT '缴费主表ID', + `transfer_type` VARCHAR(16) DEFAULT NULL COMMENT 'wxzz微信 zzbzz支付宝 yhkzz银行卡 xj现金', + `transfer_amount` DECIMAL(18,2) DEFAULT NULL COMMENT '转账金额', + `transfer_time` DATETIME DEFAULT NULL COMMENT '转账时间', + `transfer_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '转账附件逗号分隔', + `finance_confirm_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '财务确认附件逗号分隔', + `pay_status` VARCHAR(32) NOT NULL DEFAULT 'dfj' COMMENT 'dfj待缴费 jfqrz缴费确认中 yjf已缴费 tfqrz退费确认中 ytf已退费', + `installment_no` INT DEFAULT NULL COMMENT '分期期序号,趸交为空', + `installment_due_date` CHAR(8) DEFAULT NULL COMMENT '分期应缴费时间YYYYMMDD', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_payd_pay` (`payment_id`, `del_flag`), + KEY `idx_payd_status` (`tenant_no`, `pay_status`, `del_flag`), + KEY `idx_payd_due` (`installment_due_date`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-缴费明细'; + +CREATE TABLE `biz_protocol_party_customer` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `collection_protocol_id` VARCHAR(64) NOT NULL COMMENT '采集协议单ID', + `customer_id` VARCHAR(64) DEFAULT NULL COMMENT '关联客户表 customer', + `mother_name` VARCHAR(100) DEFAULT NULL COMMENT '母亲姓名', + `mother_phone` VARCHAR(32) DEFAULT NULL COMMENT '母亲联系电话', + `mother_id_card` VARCHAR(32) DEFAULT NULL COMMENT '母亲身份证号', + `mother_birth_ym` CHAR(6) DEFAULT NULL COMMENT '母亲出生年月YYYYMM', + `mother_ethnic` VARCHAR(32) DEFAULT NULL COMMENT '母亲民族', + `mother_address` VARCHAR(512) DEFAULT NULL COMMENT '母亲家庭住址', + `father_name` VARCHAR(100) DEFAULT NULL COMMENT '父亲姓名', + `father_phone` VARCHAR(32) DEFAULT NULL COMMENT '父亲电话', + `father_id_card` VARCHAR(32) DEFAULT NULL COMMENT '父亲身份证号', + `newborn_delivery_time` DATETIME DEFAULT NULL COMMENT '新生儿分娩时间', + `newborn_sex` CHAR(1) DEFAULT NULL COMMENT '1男 0女', + `delivery_mode` VARCHAR(16) DEFAULT NULL COMMENT 'sc顺产 pgc剖宫产', + `birth_order` INT DEFAULT NULL COMMENT '胎次', + `newborn_weight_gram` INT DEFAULT NULL COMMENT '新生儿体重(克)', + `apgar_1min` INT DEFAULT NULL COMMENT '新生儿评分1分钟', + `apgar_5min` INT DEFAULT NULL COMMENT '新生儿评分5分钟', + `collector_user_id` VARCHAR(64) DEFAULT NULL COMMENT '采集专员用户ID', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_party_cp` (`collection_protocol_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-当事人/产妇新生儿信息'; + +CREATE TABLE `biz_protocol_audit` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `collection_protocol_id` VARCHAR(64) NOT NULL COMMENT '采集协议单ID', + `auditor_user_id` VARCHAR(64) DEFAULT NULL COMMENT '审核人', + `audit_remark` VARCHAR(1000) DEFAULT NULL COMMENT '审核备注', + `audit_status` VARCHAR(16) NOT NULL COMMENT 'shtg审核通过 shth审核退回', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_audit_cp` (`collection_protocol_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-审核记录'; + +CREATE TABLE `biz_protocol_fullmoon_todo` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `collection_protocol_id` VARCHAR(64) NOT NULL COMMENT '采集协议单ID', + `todo_status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '1已办报销 0未办保险', + `handle_date` CHAR(8) DEFAULT NULL COMMENT '办理日期', + `handle_remark` VARCHAR(512) DEFAULT NULL COMMENT '办理备注', + `insurance_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '保险附件逗号分隔', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_fullmoon_cp` (`collection_protocol_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='新生儿满月待办'; + +CREATE TABLE `biz_protocol_storage_cert` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `collection_protocol_id` VARCHAR(64) NOT NULL COMMENT '采集协议单ID', + `make_status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '1已制作 0未制作', + `cert_image_urls` VARCHAR(4000) DEFAULT NULL COMMENT '储存证图片逗号分隔', + `make_date` CHAR(8) DEFAULT NULL COMMENT '制作日期', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_cert_cp` (`collection_protocol_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='储存证'; + +CREATE TABLE `biz_protocol_operation_log` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `collection_protocol_id` VARCHAR(64) NOT NULL COMMENT '采集协议单ID', + `operation_code` VARCHAR(64) NOT NULL COMMENT '操作编码', + `operation_name` VARCHAR(200) NOT NULL COMMENT '操作名称', + `operation_detail` VARCHAR(4000) DEFAULT NULL COMMENT '操作详情/变更摘要', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_oplog_cp` (`collection_protocol_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单操作日志'; + +-- ========== 2.3 中试服务:细胞产品与价格体系 ========== +DROP TABLE IF EXISTS `biz_cell_product_project_system_item`; +DROP TABLE IF EXISTS `biz_cell_product_project_system`; +DROP TABLE IF EXISTS `biz_cell_product_price`; +DROP TABLE IF EXISTS `biz_cell_product_spec`; +DROP TABLE IF EXISTS `biz_cell_product_type`; + +CREATE TABLE `biz_cell_product_type` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `product_type_code` VARCHAR(64) NOT NULL COMMENT '细胞产品类型编码', + `product_type_name` VARCHAR(200) NOT NULL COMMENT '细胞产品类型名称', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_cpt` (`tenant_no`, `product_type_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='细胞产品类型'; + +CREATE TABLE `biz_cell_product_spec` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `spec_code` VARCHAR(64) NOT NULL COMMENT '规格编码', + `spec_name` VARCHAR(200) NOT NULL COMMENT '规格名称', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_cps` (`tenant_no`, `spec_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='细胞产品规格'; + +CREATE TABLE `biz_cell_product_price` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `project_name` VARCHAR(200) NOT NULL COMMENT '项目名称', + `project_code` VARCHAR(64) NOT NULL COMMENT '项目编码', + `cell_type_code` VARCHAR(64) DEFAULT NULL COMMENT '细胞类型编码', + `cell_type_name` VARCHAR(200) DEFAULT NULL COMMENT '细胞类型名称', + `spec_text` VARCHAR(200) DEFAULT NULL COMMENT '规格(展示或关联规格编码)', + `spec_id` VARCHAR(64) DEFAULT NULL COMMENT '规格ID', + `cell_product_type_id` VARCHAR(64) DEFAULT NULL COMMENT '细胞产品类型ID', + `unit_price` DECIMAL(18,2) DEFAULT NULL COMMENT '单价(元)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_cpp_tenant` (`tenant_no`, `del_flag`), + KEY `idx_cpp_code` (`tenant_no`, `project_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='细胞产品价格体系'; + +CREATE TABLE `biz_cell_product_project_system` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `system_name` VARCHAR(200) NOT NULL COMMENT '体系名称', + `system_code` VARCHAR(64) NOT NULL COMMENT '体系编码', + `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '1正常 0停用', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_cpps` (`tenant_no`, `system_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='细胞产品项目价格体系'; + +CREATE TABLE `biz_cell_product_project_system_item` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `project_system_id` VARCHAR(64) NOT NULL COMMENT '项目价格体系ID', + `source_price_id` VARCHAR(64) DEFAULT NULL COMMENT '来源细胞产品价格体系行ID', + `project_name` VARCHAR(200) DEFAULT NULL COMMENT '项目名称', + `project_code` VARCHAR(64) DEFAULT NULL COMMENT '项目编码', + `cell_type_code` VARCHAR(64) DEFAULT NULL COMMENT '细胞类型编码', + `cell_type_name` VARCHAR(200) DEFAULT NULL COMMENT '细胞类型名称', + `spec_text` VARCHAR(200) DEFAULT NULL COMMENT '规格', + `spec_id` VARCHAR(64) DEFAULT NULL COMMENT '规格ID', + `cell_product_type_id` VARCHAR(64) DEFAULT NULL COMMENT '细胞产品类型ID', + `unit_price` DECIMAL(18,2) DEFAULT NULL COMMENT '单价(元)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_cppsi_sys` (`project_system_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目价格体系-细胞价格明细'; + +-- ========== 2.3.5 渠道及子表 ========== +DROP TABLE IF EXISTS `biz_channel_agreement_product_price`; +DROP TABLE IF EXISTS `biz_channel_agreement`; +DROP TABLE IF EXISTS `biz_channel_mail_address`; +DROP TABLE IF EXISTS `biz_channel_invoice`; +DROP TABLE IF EXISTS `biz_channel`; + +CREATE TABLE `biz_channel` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `channel_name` VARCHAR(200) NOT NULL COMMENT '渠道名称', + `credit_code` VARCHAR(64) DEFAULT NULL COMMENT '统一社会信用代码', + `channel_type` VARCHAR(16) DEFAULT NULL COMMENT 'yy医院 hzjg合作机构 zs诊所 zfjg政府机构 qt其他', + `contact_name` VARCHAR(100) DEFAULT NULL COMMENT '联系人姓名', + `contact_phone` VARCHAR(32) DEFAULT NULL COMMENT '联系电话', + `contact_email` VARCHAR(128) DEFAULT NULL COMMENT '联系邮箱', + `protocol_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '渠道协议附件逗号分隔', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_channel_tenant` (`tenant_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道信息'; + +CREATE TABLE `biz_channel_invoice` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `channel_id` VARCHAR(64) NOT NULL COMMENT '渠道ID', + `company_name` VARCHAR(200) NOT NULL COMMENT '单位名称', + `tax_no` VARCHAR(64) DEFAULT NULL COMMENT '税号', + `company_address` VARCHAR(512) DEFAULT NULL COMMENT '单位地址', + `company_phone` VARCHAR(64) DEFAULT NULL COMMENT '电话', + `bank_name` VARCHAR(200) DEFAULT NULL COMMENT '开户银行', + `bank_account` VARCHAR(64) DEFAULT NULL COMMENT '银行账号', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_ch_inv_channel` (`channel_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道开票信息'; + +CREATE TABLE `biz_channel_mail_address` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `channel_id` VARCHAR(64) NOT NULL COMMENT '渠道ID', + `mail_address` VARCHAR(512) NOT NULL COMMENT '邮寄地址', + `mail_contact` VARCHAR(100) DEFAULT NULL COMMENT '邮寄联系人', + `mail_phone` VARCHAR(32) DEFAULT NULL COMMENT '邮寄联系电话', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_ch_mail_channel` (`channel_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道邮寄地址'; + +CREATE TABLE `biz_channel_agreement` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `channel_id` VARCHAR(64) NOT NULL COMMENT '渠道ID', + `agreement_type` VARCHAR(16) NOT NULL COMMENT 'kyxy科研 zsxy中试 lcxy临床', + `business_type` VARCHAR(32) DEFAULT NULL COMMENT 'kyyy科研应用/中试应用/临床应用(随协议类型)', + `settle_mode` VARCHAR(16) DEFAULT NULL COMMENT 'yj月结 jj季结 nj年结 dcjs单次结算', + `credit_limit_amount` DECIMAL(18,2) DEFAULT NULL COMMENT '授信额度(元)', + `effective_date` CHAR(8) DEFAULT NULL COMMENT '生效日期YYYYMMDD', + `expire_date` CHAR(8) DEFAULT NULL COMMENT '到期日期YYYYMMDD', + `agreement_status` VARCHAR(16) NOT NULL DEFAULT 'sxz' COMMENT 'sxz生效中 yzf已作废', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_ch_agr_channel` (`channel_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道协议'; + +CREATE TABLE `biz_channel_agreement_product_price` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `channel_agreement_id` VARCHAR(64) NOT NULL COMMENT '渠道协议ID', + `project_name` VARCHAR(200) DEFAULT NULL COMMENT '项目名称', + `project_code` VARCHAR(64) DEFAULT NULL COMMENT '项目编码', + `cell_type_code` VARCHAR(64) DEFAULT NULL COMMENT '细胞类型编码', + `cell_type_name` VARCHAR(200) DEFAULT NULL COMMENT '细胞类型名称', + `spec_text` VARCHAR(200) DEFAULT NULL COMMENT '规格', + `original_price` DECIMAL(18,2) DEFAULT NULL COMMENT '原价(元)', + `agreement_price` DECIMAL(18,2) DEFAULT NULL COMMENT '协议价(元)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_cap_agr` (`channel_agreement_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道协议-细胞产品价格'; + +-- ========== 2.3.6 体检机构 ========== +DROP TABLE IF EXISTS `biz_checkup_institution_project`; +DROP TABLE IF EXISTS `biz_checkup_institution_invoice`; +DROP TABLE IF EXISTS `biz_checkup_institution`; + +CREATE TABLE `biz_checkup_institution` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `institution_name` VARCHAR(200) NOT NULL COMMENT '体检机构名称', + `institution_address` VARCHAR(512) DEFAULT NULL COMMENT '体检机构地址', + `contact_name` VARCHAR(100) DEFAULT NULL COMMENT '联系人', + `contact_phone` VARCHAR(32) DEFAULT NULL COMMENT '联系人电话', + `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '1正常 0停用', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_ck_inst` (`tenant_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检机构'; + +CREATE TABLE `biz_checkup_institution_invoice` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `checkup_institution_id` VARCHAR(64) NOT NULL COMMENT '体检机构ID', + `company_name` VARCHAR(200) NOT NULL COMMENT '单位名称', + `tax_no` VARCHAR(64) DEFAULT NULL COMMENT '税号', + `company_address` VARCHAR(512) DEFAULT NULL COMMENT '单位地址', + `company_phone` VARCHAR(64) DEFAULT NULL COMMENT '电话', + `bank_name` VARCHAR(200) DEFAULT NULL COMMENT '开户银行', + `bank_account` VARCHAR(64) DEFAULT NULL COMMENT '银行账号', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_ck_inv` (`checkup_institution_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检机构开票信息'; + +CREATE TABLE `biz_checkup_institution_project` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `checkup_institution_id` VARCHAR(64) NOT NULL COMMENT '体检机构ID', + `project_name` VARCHAR(200) NOT NULL COMMENT '项目名称', + `project_fee` DECIMAL(18,2) DEFAULT NULL COMMENT '项目费用(元)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_ck_proj` (`checkup_institution_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检机构项目及价格'; + +-- ========== 2.3.7 / 2.3.8 渠道服务购买订单及财务确认 ========== +DROP TABLE IF EXISTS `biz_channel_service_order_finance_confirm`; +DROP TABLE IF EXISTS `biz_channel_service_order_product`; +DROP TABLE IF EXISTS `biz_channel_service_order`; + +CREATE TABLE `biz_channel_service_order` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `channel_id` VARCHAR(64) NOT NULL COMMENT '渠道ID', + `channel_name` VARCHAR(200) DEFAULT NULL COMMENT '渠道名称冗余', + `dept_name` VARCHAR(200) DEFAULT NULL COMMENT '科室/部门', + `order_type` VARCHAR(16) NOT NULL COMMENT 'yfk预付款 gmcp购买产品', + `prepay_amount` DECIMAL(18,2) DEFAULT NULL COMMENT '预付金额(订单类型yfk)', + `purchase_total_amount` DECIMAL(18,2) DEFAULT NULL COMMENT '购买总金额(订单类型gmcp)', + `order_status` VARCHAR(16) NOT NULL DEFAULT 'sxz' COMMENT 'sxz生效中 yzf已作废', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注信息', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_cso_channel` (`channel_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道服务购买订单'; + +CREATE TABLE `biz_channel_service_order_product` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `service_order_id` VARCHAR(64) NOT NULL COMMENT '渠道服务购买订单ID', + `project_code` VARCHAR(64) DEFAULT NULL COMMENT '项目编码', + `project_name` VARCHAR(200) DEFAULT NULL COMMENT '项目名称', + `cell_type` VARCHAR(200) DEFAULT NULL COMMENT '细胞类型', + `spec_text` VARCHAR(200) DEFAULT NULL COMMENT '规格', + `agreement_unit_price` DECIMAL(18,2) DEFAULT NULL COMMENT '协议单价', + `buy_qty` DECIMAL(18,4) DEFAULT NULL COMMENT '购买数量', + `buy_amount` DECIMAL(18,2) DEFAULT NULL COMMENT '购买金额', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_csop_order` (`service_order_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道服务购买订单-产品明细'; + +CREATE TABLE `biz_channel_service_order_finance_confirm` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `service_order_id` VARCHAR(64) NOT NULL COMMENT '渠道服务购买订单ID', + `confirm_time` DATETIME DEFAULT NULL COMMENT '确认时间', + `confirm_desc` VARCHAR(2000) DEFAULT NULL COMMENT '确认描述', + `confirm_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '确认附件逗号分隔', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_csofc_order` (`service_order_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道服务购买订单-财务确认'; + +-- ========== 2.3.9 中试申请单及嵌套子表 ========== +DROP TABLE IF EXISTS `biz_pilot_checkup_actual_item`; +DROP TABLE IF EXISTS `biz_pilot_checkup_reserve_item`; +DROP TABLE IF EXISTS `biz_pilot_checkup`; +DROP TABLE IF EXISTS `biz_pilot_delivery_method`; +DROP TABLE IF EXISTS `biz_pilot_delivery_material`; +DROP TABLE IF EXISTS `biz_pilot_delivery`; +DROP TABLE IF EXISTS `biz_pilot_infusion_confirm`; +DROP TABLE IF EXISTS `biz_pilot_infusion_schedule`; +DROP TABLE IF EXISTS `biz_pilot_infusion_plan`; +DROP TABLE IF EXISTS `biz_pilot_patient`; +DROP TABLE IF EXISTS `biz_pilot_application`; + +CREATE TABLE `biz_pilot_application` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `channel_service_order_id` VARCHAR(64) NOT NULL COMMENT '渠道服务购买订单ID', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_pilot_app_order` (`channel_service_order_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道中试服务申请单'; + +CREATE TABLE `biz_pilot_patient` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `pilot_application_id` VARCHAR(64) NOT NULL COMMENT '中试申请单ID', + `customer_id` VARCHAR(64) DEFAULT NULL COMMENT '关联客户ID', + `patient_name` VARCHAR(100) DEFAULT NULL COMMENT '姓名', + `sex` CHAR(1) DEFAULT NULL COMMENT '性别', + `age` INT DEFAULT NULL COMMENT '年龄', + `id_card_no` VARCHAR(32) DEFAULT NULL COMMENT '身份证号', + `phone_no` VARCHAR(32) DEFAULT NULL COMMENT '联系电话', + `emergency_contact` VARCHAR(100) DEFAULT NULL COMMENT '紧急联系人', + `emergency_phone` VARCHAR(32) DEFAULT NULL COMMENT '紧急联系人电话', + `clinical_diagnosis` VARCHAR(2000) DEFAULT NULL COMMENT '临床诊断', + `diagnosis_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '诊断附件逗号分隔', + `need_checkup` TINYINT NOT NULL DEFAULT 1 COMMENT '是否需要体检 1需要 0不需要', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_pilot_patient_app` (`pilot_application_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='中试申请单-患者信息'; + +CREATE TABLE `biz_pilot_infusion_plan` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `pilot_application_id` VARCHAR(64) NOT NULL COMMENT '中试申请单ID', + `cell_type` VARCHAR(200) DEFAULT NULL COMMENT '细胞类型', + `spec_text` VARCHAR(200) DEFAULT NULL COMMENT '规格', + `total_course_count` INT DEFAULT NULL COMMENT '总疗程数', + `course_interval` VARCHAR(200) DEFAULT NULL COMMENT '疗程间隔描述', + `infusion_route` VARCHAR(200) DEFAULT NULL COMMENT '输注途径', + `infusion_start_date` CHAR(8) DEFAULT NULL COMMENT '输注开始日期YYYYMMDD', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_pip_app` (`pilot_application_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='中试申请单-输注方案'; + +CREATE TABLE `biz_pilot_infusion_schedule` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `infusion_plan_id` VARCHAR(64) NOT NULL COMMENT '输注方案ID', + `cell_type` VARCHAR(200) DEFAULT NULL COMMENT '细胞类型', + `spec_text` VARCHAR(200) DEFAULT NULL COMMENT '规格', + `infusion_time` DATETIME DEFAULT NULL COMMENT '输注时间', + `schedule_status` VARCHAR(16) NOT NULL DEFAULT 'dsc' COMMENT 'dsc待生产 ysc已生产 yjf已交付 yqx已取消', + `attachments` VARCHAR(4000) DEFAULT NULL COMMENT '附件逗号分隔', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_pis_plan` (`infusion_plan_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='中试申请单-输注计划'; + +CREATE TABLE `biz_pilot_infusion_confirm` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `infusion_schedule_id` VARCHAR(64) NOT NULL COMMENT '输注计划ID', + `confirm_user_id` VARCHAR(64) DEFAULT NULL COMMENT '确认人', + `confirm_time` DATETIME DEFAULT NULL COMMENT '确认时间', + `confirm_desc` VARCHAR(2000) DEFAULT NULL COMMENT '确认描述', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_pic_sched` (`infusion_schedule_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='输注计划-应用确认'; + +CREATE TABLE `biz_pilot_delivery` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `infusion_schedule_id` VARCHAR(64) NOT NULL COMMENT '输注计划ID', + `delivery_no` VARCHAR(64) DEFAULT NULL COMMENT '交付编号(可程序生成)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_pdel_sched` (`infusion_schedule_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='输注计划-交付信息'; + +CREATE TABLE `biz_pilot_delivery_material` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `delivery_id` VARCHAR(64) NOT NULL COMMENT '交付信息ID', + `material_name` VARCHAR(200) NOT NULL COMMENT '物料名称', + `qty` DECIMAL(18,4) DEFAULT NULL COMMENT '数量', + `unit_id` VARCHAR(64) DEFAULT NULL COMMENT '单位ID', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_pdm_del` (`delivery_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='交付信息-物料清单'; + +CREATE TABLE `biz_pilot_delivery_method` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `delivery_id` VARCHAR(64) NOT NULL COMMENT '交付信息ID', + `delivery_mode` VARCHAR(16) NOT NULL COMMENT 'qdzt渠道自提 kdfh快递发货', + `delivery_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '交付附件', + `ship_time` DATETIME DEFAULT NULL COMMENT '发运时间', + `handover_temp` VARCHAR(64) DEFAULT NULL COMMENT '交接温度', + `pickup_person` VARCHAR(100) DEFAULT NULL COMMENT '自提人员', + `pickup_phone` VARCHAR(32) DEFAULT NULL COMMENT '联系电话', + `sender_name` VARCHAR(100) DEFAULT NULL COMMENT '发货人', + `express_company_id` VARCHAR(64) DEFAULT NULL COMMENT '快递公司ID', + `waybill_no` VARCHAR(128) DEFAULT NULL COMMENT '快递单号', + `express_fee` DECIMAL(18,2) DEFAULT NULL COMMENT '快递费用', + `eta_time` DATETIME DEFAULT NULL COMMENT '预计到达时间', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_pdmeth_del` (`delivery_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='交付信息-交付方式'; + +CREATE TABLE `biz_pilot_checkup` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `pilot_application_id` VARCHAR(64) NOT NULL COMMENT '中试申请单ID', + `checkup_institution_id` VARCHAR(64) DEFAULT NULL COMMENT '体检机构ID', + `reserve_time` DATETIME DEFAULT NULL COMMENT '预约时间', + `reserve_fee` DECIMAL(18,2) DEFAULT NULL COMMENT '预约费用', + `actual_checkup_time` DATETIME DEFAULT NULL COMMENT '实际体检时间', + `actual_checkup_fee` DECIMAL(18,2) DEFAULT NULL COMMENT '实际体检费用', + `fee_settled` TINYINT DEFAULT 0 COMMENT '费用是否结算 1是 0否', + `checkup_status` VARCHAR(16) DEFAULT 'dtj' COMMENT 'dtj待体检 ytj已体检', + `fee_status` VARCHAR(16) DEFAULT 'wjs' COMMENT 'wjs未结算 yjs已结算', + `checkup_remark` VARCHAR(2000) DEFAULT NULL COMMENT '体检备注', + `report_attachments` VARCHAR(4000) DEFAULT NULL COMMENT '体检报告附件', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_pchk_app` (`pilot_application_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='中试申请单-体检信息'; + +CREATE TABLE `biz_pilot_checkup_reserve_item` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `pilot_checkup_id` VARCHAR(64) NOT NULL COMMENT '中试申请单体检信息ID', + `project_name` VARCHAR(200) DEFAULT NULL COMMENT '项目名称', + `project_fee` DECIMAL(18,2) DEFAULT NULL COMMENT '项目费用(元)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_pcri_chk` (`pilot_checkup_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检预约项目明细'; + +CREATE TABLE `biz_pilot_checkup_actual_item` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `pilot_checkup_id` VARCHAR(64) NOT NULL COMMENT '中试申请单体检信息ID', + `project_name` VARCHAR(200) DEFAULT NULL COMMENT '项目名称', + `project_fee` DECIMAL(18,2) DEFAULT NULL COMMENT '项目费用(元)', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_pcai_chk` (`pilot_checkup_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检实际项目明细'; + +-- ========== 2.4 运营支撑 ========== +DROP TABLE IF EXISTS `biz_clean_room_access_log`; +DROP TABLE IF EXISTS `biz_clean_room`; +CREATE TABLE `biz_clean_room` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `room_code` VARCHAR(64) NOT NULL COMMENT '编号', + `room_name` VARCHAR(200) NOT NULL COMMENT '名称', + `room_grade` VARCHAR(8) DEFAULT NULL COMMENT '等级 A/B/C/D', + `room_status` VARCHAR(16) NOT NULL DEFAULT 'zc' COMMENT 'zc正常 yc异常', + `area_sqm` DECIMAL(12,2) DEFAULT NULL COMMENT '面积m²', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_clean_code` (`tenant_no`, `room_code`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='洁净室信息'; + +CREATE TABLE `biz_clean_room_access_log` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `clean_room_id` VARCHAR(64) NOT NULL COMMENT '洁净室ID', + `user_id` VARCHAR(64) DEFAULT NULL COMMENT '人员用户ID', + `person_name` VARCHAR(100) DEFAULT NULL COMMENT '人员姓名(冗余)', + `dept_name` VARCHAR(200) DEFAULT NULL COMMENT '部门', + `enter_time` DATETIME NOT NULL COMMENT '进入时间', + `purpose` VARCHAR(512) DEFAULT NULL COMMENT '目的', + `leave_time` DATETIME DEFAULT NULL COMMENT '离开时间', + `body_temp` VARCHAR(32) DEFAULT NULL COMMENT '体温', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_crlog_room` (`clean_room_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='洁净室人员进出'; + +DROP TABLE IF EXISTS `biz_storage_facility_slot`; +DROP TABLE IF EXISTS `biz_storage_facility`; +CREATE TABLE `biz_storage_facility` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `parent_id` VARCHAR(64) NOT NULL DEFAULT '0' COMMENT '父节点ID,0根', + `node_name` VARCHAR(200) NOT NULL COMMENT '节点名称(罐/层/盒等)', + `node_type` VARCHAR(32) DEFAULT NULL COMMENT 'facility/layer/box 等扩展', + `is_leaf` TINYINT NOT NULL DEFAULT 0 COMMENT '是否叶子存储位 1是', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_sf_parent` (`parent_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='细胞存储设施树节点'; + +CREATE TABLE `biz_storage_facility_slot` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `facility_node_id` VARCHAR(64) NOT NULL COMMENT '设施树叶子节点ID', + `slot_code` VARCHAR(64) DEFAULT NULL COMMENT '小格编码/坐标', + `slot_status` VARCHAR(16) DEFAULT NULL COMMENT '占用状态等扩展', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_sfs_node` (`facility_node_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='细胞存储设施储存位置(格子)'; + +DROP TABLE IF EXISTS `biz_exhibition_appointment`; +CREATE TABLE `biz_exhibition_appointment` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键ID', + `appointment_no` VARCHAR(64) NOT NULL COMMENT '预约编号', + `visit_org` VARCHAR(200) DEFAULT NULL COMMENT '来访单位', + `contact_name` VARCHAR(100) DEFAULT NULL COMMENT '联系人', + `visit_date` CHAR(8) DEFAULT NULL COMMENT '到访日期YYYYMMDD', + `visitor_count` INT DEFAULT NULL COMMENT '人数', + `reception_user_id` VARCHAR(64) DEFAULT NULL COMMENT '接待人员用户ID', + `tenant_no` VARCHAR(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` INT DEFAULT NULL COMMENT '排序号', + `remark` VARCHAR(512) DEFAULT NULL COMMENT '备注', + `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人id', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_exh_no` (`tenant_no`, `appointment_no`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='展馆预约'; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ----------------------------------------------------------------------------- +-- 可选:租户与字典种子(可重复执行:不存在才插入) +-- ----------------------------------------------------------------------------- +INSERT INTO `sys_tenant` (`id`, `tenant_no`, `tenant_name`, `contact_name`, `contact_phone`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), 'zkxw', '中科兴皖', NULL, NULL, '', 0 +FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_tenant` `t` WHERE `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); + +INSERT INTO `sys_dict_type` (`id`, `dict_name`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '组织类别', 'zzlbbm', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_type` `t` WHERE `t`.`dict_type` = 'zzlbbm' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_type` (`id`, `dict_name`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '站点类型', 'zdlx', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_type` `t` WHERE `t`.`dict_type` = 'zdlx' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_type` (`id`, `dict_name`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '站点业务范围', 'zdywfw', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_type` `t` WHERE `t`.`dict_type` = 'zdywfw' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_type` (`id`, `dict_name`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '部门状态', 'bmzt', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_type` `t` WHERE `t`.`dict_type` = 'bmzt' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); + +INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '公司', 'gs', 'zzlbbm', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` `t` WHERE `t`.`dict_type` = 'zzlbbm' AND `t`.`dict_value` = 'gs' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '分公司', 'fgs', 'zzlbbm', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` `t` WHERE `t`.`dict_type` = 'zzlbbm' AND `t`.`dict_value` = 'fgs' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '部门', 'bm', 'zzlbbm', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` `t` WHERE `t`.`dict_type` = 'zzlbbm' AND `t`.`dict_value` = 'bm' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '中心基地', 'zxjd', 'zdlx', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` `t` WHERE `t`.`dict_type` = 'zdlx' AND `t`.`dict_value` = 'zxjd' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '样本采集', 'ybcj', 'zdywfw', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` `t` WHERE `t`.`dict_type` = 'zdywfw' AND `t`.`dict_value` = 'ybcj' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '样本制备', 'ybzb', 'zdywfw', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` `t` WHERE `t`.`dict_type` = 'zdywfw' AND `t`.`dict_value` = 'ybzb' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '质量检测', 'zljc', 'zdywfw', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` `t` WHERE `t`.`dict_type` = 'zdywfw' AND `t`.`dict_value` = 'zljc' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '细胞存储', 'xbcc', 'zdywfw', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` `t` WHERE `t`.`dict_type` = 'zdywfw' AND `t`.`dict_value` = 'xbcc' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '停用', '0', 'bmzt', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` `t` WHERE `t`.`dict_type` = 'bmzt' AND `t`.`dict_value` = '0' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); +INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `dict_type`, `tenant_no`, `create_dept_id`, `del_flag`) +SELECT REPLACE(UUID(), '-', ''), '正常', '1', 'bmzt', 'zkxw', '', 0 FROM (SELECT 1 AS `x`) `d` +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` `t` WHERE `t`.`dict_type` = 'bmzt' AND `t`.`dict_value` = '1' AND `t`.`tenant_no` = 'zkxw' AND `t`.`del_flag` = 0 LIMIT 1); diff --git a/docs/web-admin-ui-spec-fresh-mint.md b/docs/web-admin-ui-spec-fresh-mint.md index b3be54c8..72502c00 100644 --- a/docs/web-admin-ui-spec-fresh-mint.md +++ b/docs/web-admin-ui-spec-fresh-mint.md @@ -2,6 +2,8 @@ > **设计源文件**:`fresh-mint-green.html`(中科兴皖一体化门户 · 配送调度演示页) > **适用范围**:`web-admin` 工程(Vue 3 + Element Plus + SCSS/Tailwind)。实现时通过 **CSS 变量 + Element 主题变量 + 少量全局覆盖** 对齐本规范;**类名可与 demo 不同,但视觉参数以下文为准**。 +> +> **Hash 路由约定(与当前工程一致)**:模板与参考页统一挂在 **`#/demo/*`**(侧栏「Demo」菜单)。下文凡出现 **`#/demo/console`** 指工作台;**`#/demo/unit` / `product` / `category` / `user` / `order`** 等同理。§13 修订记录中仍可能出现的 **`#/system/*`、`#/delivery/*`、`#/business/*`、`#/order/supplier-settlement`、`#/order/client-settlement`** 等为**历史路由**(对应页面可能已裁剪),实现时以 **仍在仓库内的 Demo 页** 与组件源码为准。 --- @@ -202,7 +204,7 @@ - **`.stat .hint`**:`font-size: 11px`,`color: var(--text-muted)`,`margin-top: 4px` - **`.stat.warn`**:图标区背景 `#fffbeb`,边框 `rgba(217,119,6,0.2)`,图标与数值/ hint 色 `#d97706`(见 §2) -**工作台**(`#/dashboard/console`,根容器 **`console-container`**):页头 **`page-head` + `h1` + `page-desc`(§4.1)**;**时效预警条**与全局 **`console-container .alert-bar`** 一致(琥珀底 **`#fffbeb`**、边 **`#fde68a`**)。**数据概览(指标区)**的盒型与 **`fresh-mint-green.html`** 的 **`.stats` / `.stat`** 同构:**`stat-hd` + `stat-ic`**(默认 **`#ecfdf5` / `#059669`**,**`44×44px`**)+ **`stat-main`**(**`.k`** 指标名、**`.stat-side`** 内 **`.v` / `.hint`**);**待配送**等需强调预警的卡片加 **`.warn`**(图标区 **`#fffbeb` / `#d97706`**,**`.v` 与 `.hint`** 同步琥珀色,与 demo **`.stat.warn`** 一致)。**不含**独立「快捷菜单」区块(导航以侧栏为准)。**近30日趋势**:**`trend-charts-panel`** 内 **`trend-charts-grid`** 双列(窄屏单列)**`ArtLineChart`**(面积浅绿),分别展示每日营业额(元)与每日订单总数(笔),数据 **`GET /web/admin/statistics/orders/trend?days=30`**。**今日各区域与客户**:**`region-today-panel`** 内 **`region-today-split`** 双列(窄屏单列);**`title-bar-heading`** 用于「近30日趋势」「今日各区域」等标题时由 **`App.vue`** 规则切换为绿色竖条(**`::before`**),与 **`h2.title-bar-heading`**(**`app.scss`**)一致。左:**`RegionTodayDualBarChart`**(区域营业额 + 订单笔,**`/statistics/orders/today-by-region`**);右:**`RegionTodayDualBarChart`**(客户下单金额 + 商品数量,**`/statistics/orders/today-by-client`**),子标题 **`region-today-subtitle`** 带绿色小竖条。**最新订单**:**`section.panel`** + **`panel-top`** + **`list-body list-wrap`**(顶 **`border-top`**、**`list-wrap`** **`border-radius: 0 0 10px`** 与业务列表页一致)+ **`el-table`** **`list-table` + `stripe`**(**勿**加 **`border`**,与 **`#/order/order`** 等主表一致),**`row-key="id"`**,金额列 **`#059669`**;**「查看更多」**为 **`type="primary"` `link`**。全局 **`el-ui.scss`**:**`console-container .el-table`** 与 **`page-container .el-table`** 共用表头/hover token;**`console-container .el-button--primary`** 与菜单页主按钮同色链一致。 +**工作台**(`#/demo/console`;**当前仓库为轻量入口页**,以下统计/图表白述为**历史版式参考**。根容器 **`console-container`**):页头 **`page-head` + `h1` + `page-desc`(§4.1)**;**时效预警条**与全局 **`console-container .alert-bar`** 一致(琥珀底 **`#fffbeb`**、边 **`#fde68a`**)。**数据概览(指标区)**的盒型与 **`fresh-mint-green.html`** 的 **`.stats` / `.stat`** 同构:**`stat-hd` + `stat-ic`**(默认 **`#ecfdf5` / `#059669`**,**`44×44px`**)+ **`stat-main`**(**`.k`** 指标名、**`.stat-side`** 内 **`.v` / `.hint`**);**待配送**等需强调预警的卡片加 **`.warn`**(图标区 **`#fffbeb` / `#d97706`**,**`.v` 与 `.hint`** 同步琥珀色,与 demo **`.stat.warn`** 一致)。**不含**独立「快捷菜单」区块(导航以侧栏为准)。**近30日趋势**:**`trend-charts-panel`** 内 **`trend-charts-grid`** 双列(窄屏单列)**`ArtLineChart`**(面积浅绿),分别展示每日营业额(元)与每日订单总数(笔),数据 **`GET /web/admin/statistics/orders/trend?days=30`**。**今日各区域与客户**:**`region-today-panel`** 内 **`region-today-split`** 双列(窄屏单列);**`title-bar-heading`** 用于「近30日趋势」「今日各区域」等标题时由 **`App.vue`** 规则切换为绿色竖条(**`::before`**),与 **`h2.title-bar-heading`**(**`app.scss`**)一致。左:**`RegionTodayDualBarChart`**(区域营业额 + 订单笔,**`/statistics/orders/today-by-region`**);右:**`RegionTodayDualBarChart`**(客户下单金额 + 商品数量,**`/statistics/orders/today-by-client`**),子标题 **`region-today-subtitle`** 带绿色小竖条。**最新订单**:**`section.panel`** + **`panel-top`** + **`list-body list-wrap`**(顶 **`border-top`**、**`list-wrap`** **`border-radius: 0 0 10px`** 与业务列表页一致)+ **`el-table`** **`list-table` + `stripe`**(**勿**加 **`border`**,与 **`#/demo/order`** 等主表一致),**`row-key="id"`**,金额列 **`#059669`**;**「查看更多」**为 **`type="primary"` `link`**。全局 **`el-ui.scss`**:**`console-container .el-table`** 与 **`page-container .el-table`** 共用表头/hover token;**`console-container .el-button--primary`** 与菜单页主按钮同色链一致。 ### 4.3 面板 `.panel` / `.panel-top` @@ -254,7 +256,7 @@ - **场景**:检索条件为**多级分类**等树形数据时,**下拉树节点**仅展示**本级名称**(结构已表达层级,避免每行重复冗长前缀);选中收起后,输入框内回显 **自根到当前节点的全路径**(如 `一级分类 / 二级分类 / 三级分类`),避免仅末级名称产生歧义。 - **实现要点**:树数据里 **`label` 仍用本级名称**。`el-tree-select` 内部基于 `ElSelect`,可将 **`#label` 插槽** 透传至 Select,在插槽内根据当前选中 `value`(节点 id)在**扁平分类列表**中沿 `parentId` 向上拼接全路径作为展示文案;**`filter-node-method`** 建议同时匹配本级名与全路径,便于用上级关键词检索。 -- **落地参考**:`#/base/product` 筛选与表单「商品分类」。 +- **落地参考**:`#/demo/product` 筛选与表单「商品分类」。 ### 5.6 树形业务数据全链路展示规律(通用,不限于商品分类) @@ -276,7 +278,7 @@ - 需要 **「有路径用路径,否则用接口给的末级名」**:使用 **`resolveTreePathDisplay`**。 - 仅有末级名、且 **无** `id`、无法关联主数据时:只能展示接口文案;**推荐接口**在可行时返回 `categoryId`(或对应树节点 id)以便前端拼路径。 -**已对齐页面(随改造持续补充)**:`#/base/product`(列表列、筛选/表单树选、导出、查看弹窗组件)、`#/business/inquiry-quotation`(分类列/卡片等使用分类树路径映射)。 +**已对齐页面(随改造持续补充)**:`#/demo/product`(列表列、筛选/表单树选、导出、查看弹窗组件);原询价报价页已裁剪,分类树路径映射与 **`#/demo/category`** / **`#/demo/product`** 一致。 --- @@ -420,7 +422,7 @@ ### 7.9 分页(Element Plus:`el-pagination` 与 `.pagination.list-pager-bar`) -`web-admin` 列表页(如 **单位管理** `#/base/unit`、**商品管理** `#/base/product`)在 **`el-table` 下方**放置 **`el-pagination`**,外层使用 **同一套容器类**,保证与表格同属一块白底面板、顶部分割线清晰,**右对齐**控件区(总数、每页条数、翻页、跳转)。 +`web-admin` 列表页(如 **单位管理** `#/demo/unit`、**商品管理** `#/demo/product`)在 **`el-table` 下方**放置 **`el-pagination`**,外层使用 **同一套容器类**,保证与表格同属一块白底面板、顶部分割线清晰,**右对齐**控件区(总数、每页条数、翻页、跳转)。 **容器(推荐类名)**:**`.pagination.list-pager-bar`**(可同时保留 **`.pagination`** 单类,但样式以下列为准)。 @@ -435,7 +437,7 @@ **结构约定**:分页与表格包在同一 **`.list-body.list-wrap`**(或等价)内,使 **`.list-wrap`** 的 **`border-radius: 0 0 10px 10px`** 覆盖整块「表 + 分页」;分页条上边框与表体底边衔接,形成明确的「列表分页区」。 -**对齐参考页**:`#/base/unit`;其他业务列表页改造时应复用上述容器类与数值,避免各页自定义分页外边距或缺上边框。 +**对齐参考页**:`#/demo/unit`;其他业务列表页改造时应复用上述容器类与数值,避免各页自定义分页外边距或缺上边框。 --- @@ -529,7 +531,7 @@ - **底部动作**:`dialog-foot` 固定在底部,查看弹窗仅放“关闭/返回”类动作,避免在查看态塞入提交语义。 - **交互与可访问性**:支持点击遮罩关闭、`Esc` 关闭;滚动仅发生在 `dialog-scroll`;图片提供可预览与 `alt`,长文本按省略/换行策略保持稳定排版。 - **组件抽象建议**:可统一封装为业务级查看组件(如 `ProductViewDialog`),内部固定使用 `view-hero / view-section / view-kv-grid / dialog-foot` 语义类,业务页只传入数据与开关状态,避免每页重复造样式。 -- **落地示例**:`#/base/product` 已按该模板改造(`web-admin/src/components/business/product-view-dialog/index.vue`),可直接作为后续页面的实现参考。 +- **落地示例**:`#/demo/product` 已按该模板改造(`web-admin/src/components/business/product-view-dialog/index.vue`),可直接作为后续页面的实现参考。 - **树形字段展示**:查看弹窗中凡**树形主数据**(分类等)均与 **§5.6** 一致,展示**全路径**;实现上优先复用 `utils/tree-path` 与已加载的扁平列表或树。 --- @@ -563,7 +565,7 @@ - **`.upload-preview`**:`flex`,`gap: 10px`,`margin-top: 12px` - **`.upload-item`**:**72×72**,圆角 **8px**,边 `var(--border)`;**`.rm`**(删除缩略图):**20×20**,绝对定位右上 **2px**,背景 `rgba(0,0,0,0.55)`,白字,圆角 **4px** -### 10.6 商品分类 · 弹窗内「参数」卡片(`#/base/category`) +### 10.6 商品分类 · 弹窗内「参数」卡片(`#/demo/category`) 单张卡片结构(自上而下): @@ -616,23 +618,23 @@ 4. 表格与 art-table:`el-table` 对齐 §7(含 sticky、图文列;**Element 列表分页条**见 **§7.9**)。 5. `el-dialog` / `el-drawer`:§9。 6. 树形页面:§8。 -7. **工作台**(`#/dashboard/console`):见 **§4.2** 工作台小节——与 **`fresh-mint-green.html`** **`.stat`** 同构的指标卡、**近30日** **`ArtLineChart`** 双图(`/statistics/orders/trend`)、**今日各区域** 双轴柱状(`/statistics/orders/today-by-region`)、**`list-body list-wrap` + `list-table` + `stripe`**(无 **`border`**)最新订单;与 **`el-ui.scss`** 中 **`console-container`** 表格/主按钮覆盖一致。 -8. **商品分类**(`#/base/category`):与 **区域管理** 同构——页头说明、筛选 §5、工具栏 §7.3(已选 + 左起操作)、`list-body` + 表格 + **Element 分页**、状态 pill 与 **Fresh Mint** token;新增/编辑/选参弹窗底栏 **`dialog-footer`** + **Close / Check** 图标,图标间距 **6px**(与工具栏一致);弹窗内 **参数卡片** 布局与 **`.parameter-assoc-form`** 见 **§10.6**;列表操作列统一为 **首个按钮主色,后续按钮中性**(父分类:新增子分类主色;编辑/删除中性。子分类:编辑主色;删除中性)。 -9. **商品单位**(`#/base/unit`):与 **区域管理** 同构(页头说明、筛选、工具栏 §7.3、`list-body` 顶部分割线、表格、分页、`scroll-to-error` + 校验失败 **`ElMessageBox.alert`**、**`unit-dialog`** 与 **两列表单**);列表操作列 **编辑主色 / 删除中性**。 -10. **商品管理**(`#/base/product`):筛选区切换为 §5 结构(非 `inline`)、工具栏升级为 §7.3(已选计数 + 左起操作)并保持 `list-body` 顶部分割线;**商品分类** 树形字段遵循 **§5.6**:下拉树节点为本级名,收起/列表/导出/查看等为 **全路径**(` / ` 分隔)。列表操作列遵循“首个按钮主色、后续中性”;新增/编辑与参数选择弹窗底栏统一 **`dialog-footer`** + **Close / Check** 图标与 **6px** 间距。商品列表补充:工具栏按钮遵循“**第一个按钮有颜色,后面的按钮都是普通按钮**”(即首个主色、其余中性描边);操作列固定右侧且预留足够宽度完整展示全部按钮;商品图片与名称合并为一列(表头居中、内容左对齐、名称超长省略)。弹窗补充:基本信息表单与商品单位页一致,采用 **label 在上、输入在下**(`label-position="top"`),避免左右并排造成间距过大;图片上传等 **primary-plain** 按钮需保证常态下图标与文字可见(非 hover 才显示);弹窗底栏取消/确定前缀图标必须可见;新增/编辑、参数选择、图片预览、视频播放等弹窗统一使用无模糊遮罩(`modal-class` + `backdrop-filter: none`);并在全局覆盖中移除 `.page-container .el-overlay, .console-container .el-overlay` 的 `backdrop-filter`,避免任何菜单页弹窗出现背景模糊。 -11. **询价报价管理**(`#/business/inquiry-quotation`):**页头** **`page-desc`**;**区域切换**为 **Mint 分段按钮**(**`region-tab-btn`**:白底描边 **`#d9e8e2`**,激活 **`#10b981`** 实心);**询价 / 报价** 两块均为 **`iq-panel`**(**`#d9e8e2`** 边、**10px** 圆角、轻阴影),卡片头 **`title-ic` + Operation / Document** 图标与 **§3** 工具栏语义一致;**询价表** **`list-table`**,**已发布行** 浅绿底 **`#ecfdf5` / hover `#d1fae5`**;操作列 **「查看」`btn-op-item primary`**,其余 **`btn-op-item`** + 前缀图标。**报价** 内 **Tab** 为 **`quote-tab-chip`**(与区域切换同构);**已选中** 说明用 **`iq-alert`**;**矩阵对比** 最低价格与 **供应商报价完整度** 色:绿 **`#059669`**、红 **`#dc2626`**、字 **`#1e293b`**(对齐运营端 token,非小程序角色色)。**历史报价** 筛选改为 **§5** 式 **`filter-block` + `history-filter-grid`(label 11px)+ `filter-actions-row`**(**`btn-ghost` / `btn-main`** + RefreshLeft / Search);**`history-filter-grid`** 桌面宽度下 **三列同排**(供应商|状态|时间),**`grid-template-columns: minmax(0,1fr) minmax(0,1fr) minmax(220px,1.35fr)`**,**勿**对日期表单项使用 **`grid-column: 1 / -1`** 独占一行。分页 **`list-pager-bar`**。**查看询价单** 弹窗顶部信息与 **§9.4–9.5** 对齐:**`inquiry-view-hero`**(**72×72** 图标区 + 标题 / 状态 **`el-tag`** / 创建时间摘要)+ **`inquiry-view-kv`** 内 **`view-kv-grid` + `view-kv`**(备注 **`view-multiline`** 跨两列),**不用** 与下方选品区混排的 **`el-descriptions` border** 块。**大弹窗**(询价表单 / 详情 / 报价明细 / 调价确认)统一 **`inquiry-quotation-dialog`** + **`modal-class="inquiry-quotation-dialog-mask"`**(无模糊遮罩),**`el-dialog__header`** 薄荷渐变底栏,**`dialog-footer`** **36px** 高、**8px** 圆角、主 **`#10b981`**;询价表单 footer **左右分布**(左侧已选数量 hint)。**内嵌选品区** 树「当前」高亮、卡片选中描边、面板边框均用 **Mint / `#d9e8e2`**,替换原默认蓝。列表/卡片中「分类」展示仍与 **§5.6** 一致(全路径、` / ` 分隔);工具函数见 **`web-admin/src/utils/tree-path.ts`**。 -12. **参数模板**(`#/base/parameter`):与 **商品单位** 同构——页头 **`page-desc`**、筛选 **§5**(`filter-grid` + `filter-actions-row`)、工具栏 **§7.3**(`list-toolbar-row`,已选 **`sel-num`** + **`toolbar-actions`** 左起)、**`list-body`** 顶部分割线、表格与分页 **`list-pager-bar`**;列表操作列 **「详情」主色、编辑/删除中性**(与 §9.8 查看优先一致)。新增/编辑弹窗 **`parameter-dialog`**:**`720px`**、**`label-position="top"`**、**`form-grid-2`**、**`scroll-to-error`** + 校验失败 **`ElMessageBox.alert`**、**`parameter-dialog-mask`** 无模糊遮罩。表单栅格 **尽量双列排布**:名称|编码;**输入类型** 与 **排序** 同一行(类型使用 **`param-type-radio-group`** 允许折行);**可选值**(非文本类型时)仍 **`form-col-span-2`**;**输入提示** 与 **是否必填** 同一行;**状态** 与 **备注**(各一列,备注为双行文本域)同一行。弹窗底栏与 **区域 / 单位 / 商品** 一致:**`dialog-footer`** 内取消为 **`btn-ghost`**、确定为 **`btn-main`** + **`type="primary"`**,**Close / Check** 图标与 **6px** 间距;非 scoped 样式中对 **`.parameter-dialog .el-dialog__footer .el-button`** 统一 **36px 高、8px 圆角、600 字重**,次要按钮白底 **`#d9e8e2`** 描边,主按钮 **`#10b981`** 色链(与 **`product-view-dialog`** 底栏规则对齐)。**详情**弹窗(**`parameter-view-dialog`**):与 **`#/base/product`** 的 **`ProductViewDialog`** 对齐——**`.el-dialog__body`** 无内边距、**`.parameter-view-dialog .dialog-scroll`** 为 **`padding: 0`**,水平留白由 **`.view-hero`**(**`padding: 20px 22px`**,与商品查看一致)与 **`.view-section`**(**`padding: 18px 22px`**、块间 **`border-bottom: 1px solid #d9e8e2`**、**`:last-child`** 去底边,见 **`product-view-dialog`**)共同承担;**禁止**在本页再写仅含 **`padding: 4px 0 8px`** 的 **`.view-section`**,否则后会覆盖前述规则,导致键值区贴边、观感拥挤。其下 **`view-kv-grid`** 展示完整字段;底栏 **关闭** 使用 **`btn-ghost`** + Close 图标。筛选「状态」与单位页一致(全部为空串,请求时转 `undefined`)。 -13. **用户管理**(`#/base/user`):与 **区域 / 单位** 同构——页头 **`page-desc`**、筛选 **§5**(四列 **`filter-grid`**:关键词支持 **Enter** 查询)、工具栏 **§7.3**(**`list-toolbar-row`**、**`sel-num`**、**`toolbar-actions`**;仅 **批量导出**、无导入模板)、**`list-body list-wrap`** + 表头/单元格 **居中**(头像列保留 **§7** 图文列语义)、分页 **`list-pager-bar`**。列表操作列:**「详情」** **`btn-op-item primary`**,**编辑 / 重置密码 / 删除** 为中性 **`btn-op-item`**;操作列 **`min-width`** 需容纳四个按钮。新增/编辑弹窗:**`user-dialog`** **`720px`**、**`modal-class="user-dialog-mask"`**、**`close-on-click-modal="false"`**、**`label-position="top"`** 的 **`user-form`**,分节 **账号信息**(仅新增:用户名|密码)**基本信息**(姓名|手机|角色|所属区域 **`clearable`**)**头像**(**`primary` `plain`** 上传 + **`btn-ghost` 清除**)**状态设置**(仅编辑);**`scroll-to-error`** + 校验失败 **`ElMessageBox.alert`**(与单位页一致)。**详情**弹窗:**`user-dialog user-view-dialog`**,**`body` 无 padding** + **`.dialog-scroll` `padding: 0`**,**`view-hero`**(左侧 **72×72** 圆角头像区 + **`view-hero-main`** 摘要)+ **`view-section`** + **`view-kv-grid`**,状态用 **`status-pill`**(禁用为 **info**,与列表一致);底栏 **关闭** **`btn-ghost`** + Close。**`user-dialog-mask`**:**`backdrop-filter: none`**。 -14. **订单列表**(`#/order/order`):页头 **`page-desc`**;**`section.panel`** 内 **`panel-top`**(**`title-ic` + Operation**);筛选 **`filter-block`**(**§5.3**:多行栅格 / **`filter-extra`** / **`filter-actions`** 之间 **无** 横向分割线,仅用间距)对齐 **`fresh-mint-green.html`**:**`filter-hint`** + 常用条件 **`filter-grid order-filter-main-grid`**(桌面 **四列** 一排)+ **`filter-extra`**(**`v-show`**:支付方式、区域)+ **`filter-actions`**(左侧 **`btn-filter-toggle`** 链式按钮:**展开更多条件 / 收起更多条件**,**`aria-expanded` + `aria-controls`**;右侧 **`btn-ghost` / `btn-main`**);重置时 **`filterExpanded=false`** 收起附加条件。关键词 **Enter** 查询。**`list-body list-wrap`** 内主表 **`order-list-table order-list-main-table list-table`**(主表单元格 flex 居中**勿**作用到嵌套表),分页 **`list-pager-bar`**。**行展开子表** 对齐 demo **树形子列表`§8`**:**`expanded-cell` 背景 `#f8fafc`**、**`order-tree-nested-inner`** 左 **`3px #10b981`** + **`order-tree-nested-label`**;子表 **`order-tree-child-table`**:**`border`**、表头底 **`#e2e8f0`**、hover **`#f1f5f9`**,首列(商品)左对齐、数值列 **`is-right`**。订单编号链式按钮 **`order-no-link`**(**`#059669`**)。操作列:**「查看/运营」`btn-op-item primary` + View**,**「分配配送员」`btn-op-item` + Bicycle**,**「审核取消」`btn-op-item op-warn` + DocumentChecked**;**`min-width`** 需容纳多枚操作钮。**订单详情(查看/运营)** **`order-detail-dialog`** + **`modal-class="order-module-dialog-mask"`**:**`el-dialog__body` `padding: 0`**,内容区 **`dialog-scroll order-detail-dialog-scroll`** 内纵向滚动;顶部 **`view-hero order-view-hero`**(**72×72** **`order-view-hero-icon` + ShoppingBag + **`view-hero-main`** 标题 / **`view-hero-tags`** / **`view-hero-meta`**),各 Tab 下区块为 **`view-section`** + **`h3`** + **`view-kv-grid` / `view-kv` / `view-multiline`**(与 **参数详情 / 询价查看** 同构,不再用 **`el-descriptions` border** 作为主信息区);底栏 **`dialog-footer`**,**关闭** **`btn-ghost` + Close**。**`el-dialog__header`** 薄荷渐变、**`el-dialog__footer`** **`#f8fafc`**;主操作 **`btn-main`**,**取消订单** **`type="danger"`**。**分配 / 审核取消 / 取消订单 / 调整 / 价格版本明细** 等辅弹窗 **`order-module-dialog`** + 同款 **`modal-class`** 与 **36px / 8px** 底栏规则(与询价大弹窗同构;无模糊遮罩)。 -15. **商家结算管理**(`#/order/supplier-settlement`):页头 **`page-desc`**;**`region-tab-strip` + `region-tab-btn`**(与询价区域切换同构);**`el-tabs` `settlement-page-tabs`**(Mint 指示条);各 Tab 内 **`section.panel`**:**`panel-top`**(**`title-ic` + Wallet / Document**)、**`filter-block`**(**三列 `filter-grid`**、**`filter-actions-row`** + **`btn-ghost` / `btn-main`** + Refresh / Search、关键词 **Enter**);**待结算** 下 **`settlement-stats`**(**`stat-card` / `stat-card--accent`**);**`list-toolbar-row` + `toolbar-actions`**;主表 **`settlement-pending-main-table settlement-root-table list-table`**;**结算单记录** 主表 **`settlement-sheets-main-table settlement-root-table list-table`**;分页 **`list-pager-bar`**。**树形多层级**(对齐 **`fresh-mint-green.html` §树形列表**):待结算订单行展开为 **子列表**(**`settlement-tree-inner`** 左 **`3px #10b981`** + **`settlement-tree-label`** + **`settlement-child-table`** **`border`**、表头 **`#e2e8f0`**);结算单记录为 **结算单 → 订单 → 商品** 三层:**L1** **`settlement-tree-l1`** 内嵌 **`settlement-mid-table`**;**L2** **`settlement-tree-l2`**(琥珀左边线 + 浅黄渐变,对应 demo **`tree-nested-deep`**)+ **`settlement-line-table`**(孙表头 **`#fef3c7`** / 字 **`#92400e`**);**`expanded-cell`** 主层 **`#f8fafc`**、订单下商品层 **`#fffbeb`**。操作列:**详情 `btn-op-item primary` + View**,**编辑 `btn-op-item op-warn` + Edit**。业务弹窗 **`order-module-dialog`** + **`order-module-dialog-mask`**;本页另附 **非 scoped** 的 **`order-module-dialog`** 规则,避免未加载订单页 chunk 时弹窗无样式。订单号链式 **`settlement-order-no-link`**。 -16. **客户结算管理**(`#/order/client-settlement`):与 **商家结算管理** 同构应用 Fresh Mint 壳层与交互(页头 `page-desc`、`region-tab-strip`、`settlement-page-tabs`、`panel`、`filter-block`、`list-toolbar-row`、`list-pager-bar`、`order-module-dialog`)。业务语义改为客户维度(客户筛选、客户结算状态、`client_settlement` 凭证业务类型、同客户约束)。树形多层级同样按 **结算单 → 订单 → 商品** 实现,沿用 **`settlement-tree-inner / settlement-tree-l1 / settlement-tree-l2`** 视觉分层:L1 薄荷左边线 + 灰蓝子表头,L2 琥珀左边线 + 浅黄渐变 + 孙表琥珀表头;主层与深层 expanded cell 分别使用 **`#f8fafc`** 与 **`#fffbeb`**,保证多层树形在客户结算页与标准 demo 统一。 +7. **工作台**(`#/demo/console`):见 **§4.2** 工作台小节——与 **`fresh-mint-green.html`** **`.stat`** 同构的指标卡、**近30日** **`ArtLineChart`** 双图(`/statistics/orders/trend`)、**今日各区域** 双轴柱状(`/statistics/orders/today-by-region`)、**`list-body list-wrap` + `list-table` + `stripe`**(无 **`border`**)最新订单;与 **`el-ui.scss`** 中 **`console-container`** 表格/主按钮覆盖一致。 +8. **商品分类**(`#/demo/category`):与 **区域管理** 同构——页头说明、筛选 §5、工具栏 §7.3(已选 + 左起操作)、`list-body` + 表格 + **Element 分页**、状态 pill 与 **Fresh Mint** token;新增/编辑/选参弹窗底栏 **`dialog-footer`** + **Close / Check** 图标,图标间距 **6px**(与工具栏一致);弹窗内 **参数卡片** 布局与 **`.parameter-assoc-form`** 见 **§10.6**;列表操作列统一为 **首个按钮主色,后续按钮中性**(父分类:新增子分类主色;编辑/删除中性。子分类:编辑主色;删除中性)。 +9. **商品单位**(`#/demo/unit`):与 **区域管理** 同构(页头说明、筛选、工具栏 §7.3、`list-body` 顶部分割线、表格、分页、`scroll-to-error` + 校验失败 **`ElMessageBox.alert`**、**`unit-dialog`** 与 **两列表单**);列表操作列 **编辑主色 / 删除中性**。 +10. **商品管理**(`#/demo/product`):筛选区切换为 §5 结构(非 `inline`)、工具栏升级为 §7.3(已选计数 + 左起操作)并保持 `list-body` 顶部分割线;**商品分类** 树形字段遵循 **§5.6**:下拉树节点为本级名,收起/列表/导出/查看等为 **全路径**(` / ` 分隔)。列表操作列遵循“首个按钮主色、后续中性”;新增/编辑与参数选择弹窗底栏统一 **`dialog-footer`** + **Close / Check** 图标与 **6px** 间距。商品列表补充:工具栏按钮遵循“**第一个按钮有颜色,后面的按钮都是普通按钮**”(即首个主色、其余中性描边);操作列固定右侧且预留足够宽度完整展示全部按钮;商品图片与名称合并为一列(表头居中、内容左对齐、名称超长省略)。弹窗补充:基本信息表单与商品单位页一致,采用 **label 在上、输入在下**(`label-position="top"`),避免左右并排造成间距过大;图片上传等 **primary-plain** 按钮需保证常态下图标与文字可见(非 hover 才显示);弹窗底栏取消/确定前缀图标必须可见;新增/编辑、参数选择、图片预览、视频播放等弹窗统一使用无模糊遮罩(`modal-class` + `backdrop-filter: none`);并在全局覆盖中移除 `.page-container .el-overlay, .console-container .el-overlay` 的 `backdrop-filter`,避免任何菜单页弹窗出现背景模糊。 +11. **询价报价管理**(历史路由 **`#/business/inquiry-quotation`**,页面已裁剪;复杂列表与弹窗可对照 **`#/demo/order`**):**页头** **`page-desc`**;**区域切换**为 **Mint 分段按钮**(**`region-tab-btn`**:白底描边 **`#d9e8e2`**,激活 **`#10b981`** 实心);**询价 / 报价** 两块均为 **`iq-panel`**(**`#d9e8e2`** 边、**10px** 圆角、轻阴影),卡片头 **`title-ic` + Operation / Document** 图标与 **§3** 工具栏语义一致;**询价表** **`list-table`**,**已发布行** 浅绿底 **`#ecfdf5` / hover `#d1fae5`**;操作列 **「查看」`btn-op-item primary`**,其余 **`btn-op-item`** + 前缀图标。**报价** 内 **Tab** 为 **`quote-tab-chip`**(与区域切换同构);**已选中** 说明用 **`iq-alert`**;**矩阵对比** 最低价格与 **供应商报价完整度** 色:绿 **`#059669`**、红 **`#dc2626`**、字 **`#1e293b`**(对齐运营端 token,非小程序角色色)。**历史报价** 筛选改为 **§5** 式 **`filter-block` + `history-filter-grid`(label 11px)+ `filter-actions-row`**(**`btn-ghost` / `btn-main`** + RefreshLeft / Search);**`history-filter-grid`** 桌面宽度下 **三列同排**(供应商|状态|时间),**`grid-template-columns: minmax(0,1fr) minmax(0,1fr) minmax(220px,1.35fr)`**,**勿**对日期表单项使用 **`grid-column: 1 / -1`** 独占一行。分页 **`list-pager-bar`**。**查看询价单** 弹窗顶部信息与 **§9.4–9.5** 对齐:**`inquiry-view-hero`**(**72×72** 图标区 + 标题 / 状态 **`el-tag`** / 创建时间摘要)+ **`inquiry-view-kv`** 内 **`view-kv-grid` + `view-kv`**(备注 **`view-multiline`** 跨两列),**不用** 与下方选品区混排的 **`el-descriptions` border** 块。**大弹窗**(询价表单 / 详情 / 报价明细 / 调价确认)统一 **`inquiry-quotation-dialog`** + **`modal-class="inquiry-quotation-dialog-mask"`**(无模糊遮罩),**`el-dialog__header`** 薄荷渐变底栏,**`dialog-footer`** **36px** 高、**8px** 圆角、主 **`#10b981`**;询价表单 footer **左右分布**(左侧已选数量 hint)。**内嵌选品区** 树「当前」高亮、卡片选中描边、面板边框均用 **Mint / `#d9e8e2`**,替换原默认蓝。列表/卡片中「分类」展示仍与 **§5.6** 一致(全路径、` / ` 分隔);工具函数见 **`web-admin/src/utils/tree-path.ts`**。 +12. **参数模板**(独立菜单已裁剪;列表与弹窗版式同 **`#/demo/unit`** 等 CRUD 清单页):与 **商品单位** 同构——页头 **`page-desc`**、筛选 **§5**(`filter-grid` + `filter-actions-row`)、工具栏 **§7.3**(`list-toolbar-row`,已选 **`sel-num`** + **`toolbar-actions`** 左起)、**`list-body`** 顶部分割线、表格与分页 **`list-pager-bar`**;列表操作列 **「详情」主色、编辑/删除中性**(与 §9.8 查看优先一致)。新增/编辑弹窗 **`parameter-dialog`**:**`720px`**、**`label-position="top"`**、**`form-grid-2`**、**`scroll-to-error`** + 校验失败 **`ElMessageBox.alert`**、**`parameter-dialog-mask`** 无模糊遮罩。表单栅格 **尽量双列排布**:名称|编码;**输入类型** 与 **排序** 同一行(类型使用 **`param-type-radio-group`** 允许折行);**可选值**(非文本类型时)仍 **`form-col-span-2`**;**输入提示** 与 **是否必填** 同一行;**状态** 与 **备注**(各一列,备注为双行文本域)同一行。弹窗底栏与 **区域 / 单位 / 商品** 一致:**`dialog-footer`** 内取消为 **`btn-ghost`**、确定为 **`btn-main`** + **`type="primary"`**,**Close / Check** 图标与 **6px** 间距;非 scoped 样式中对 **`.parameter-dialog .el-dialog__footer .el-button`** 统一 **36px 高、8px 圆角、600 字重**,次要按钮白底 **`#d9e8e2`** 描边,主按钮 **`#10b981`** 色链(与 **`product-view-dialog`** 底栏规则对齐)。**详情**弹窗(**`parameter-view-dialog`**):与 **`#/demo/product`** 的 **`ProductViewDialog`** 对齐——**`.el-dialog__body`** 无内边距、**`.parameter-view-dialog .dialog-scroll`** 为 **`padding: 0`**,水平留白由 **`.view-hero`**(**`padding: 20px 22px`**,与商品查看一致)与 **`.view-section`**(**`padding: 18px 22px`**、块间 **`border-bottom: 1px solid #d9e8e2`**、**`:last-child`** 去底边,见 **`product-view-dialog`**)共同承担;**禁止**在本页再写仅含 **`padding: 4px 0 8px`** 的 **`.view-section`**,否则后会覆盖前述规则,导致键值区贴边、观感拥挤。其下 **`view-kv-grid`** 展示完整字段;底栏 **关闭** 使用 **`btn-ghost`** + Close 图标。筛选「状态」与单位页一致(全部为空串,请求时转 `undefined`)。 +13. **用户管理**(`#/demo/user`):与 **区域 / 单位** 同构——页头 **`page-desc`**、筛选 **§5**(四列 **`filter-grid`**:关键词支持 **Enter** 查询)、工具栏 **§7.3**(**`list-toolbar-row`**、**`sel-num`**、**`toolbar-actions`**;仅 **批量导出**、无导入模板)、**`list-body list-wrap`** + 表头/单元格 **居中**(头像列保留 **§7** 图文列语义)、分页 **`list-pager-bar`**。列表操作列:**「详情」** **`btn-op-item primary`**,**编辑 / 重置密码 / 删除** 为中性 **`btn-op-item`**;操作列 **`min-width`** 需容纳四个按钮。新增/编辑弹窗:**`user-dialog`** **`720px`**、**`modal-class="user-dialog-mask"`**、**`close-on-click-modal="false"`**、**`label-position="top"`** 的 **`user-form`**,分节 **账号信息**(仅新增:用户名|密码)**基本信息**(姓名|手机|角色|所属区域 **`clearable`**)**头像**(**`primary` `plain`** 上传 + **`btn-ghost` 清除**)**状态设置**(仅编辑);**`scroll-to-error`** + 校验失败 **`ElMessageBox.alert`**(与单位页一致)。**详情**弹窗:**`user-dialog user-view-dialog`**,**`body` 无 padding** + **`.dialog-scroll` `padding: 0`**,**`view-hero`**(左侧 **72×72** 圆角头像区 + **`view-hero-main`** 摘要)+ **`view-section`** + **`view-kv-grid`**,状态用 **`status-pill`**(禁用为 **info**,与列表一致);底栏 **关闭** **`btn-ghost`** + Close。**`user-dialog-mask`**:**`backdrop-filter: none`**。 +14. **订单列表**(`#/demo/order`):页头 **`page-desc`**;**`section.panel`** 内 **`panel-top`**(**`title-ic` + Operation**);筛选 **`filter-block`**(**§5.3**:多行栅格 / **`filter-extra`** / **`filter-actions`** 之间 **无** 横向分割线,仅用间距)对齐 **`fresh-mint-green.html`**:**`filter-hint`** + 常用条件 **`filter-grid order-filter-main-grid`**(桌面 **四列** 一排)+ **`filter-extra`**(**`v-show`**:支付方式、区域)+ **`filter-actions`**(左侧 **`btn-filter-toggle`** 链式按钮:**展开更多条件 / 收起更多条件**,**`aria-expanded` + `aria-controls`**;右侧 **`btn-ghost` / `btn-main`**);重置时 **`filterExpanded=false`** 收起附加条件。关键词 **Enter** 查询。**`list-body list-wrap`** 内主表 **`order-list-table order-list-main-table list-table`**(主表单元格 flex 居中**勿**作用到嵌套表),分页 **`list-pager-bar`**。**行展开子表** 对齐 demo **树形子列表`§8`**:**`expanded-cell` 背景 `#f8fafc`**、**`order-tree-nested-inner`** 左 **`3px #10b981`** + **`order-tree-nested-label`**;子表 **`order-tree-child-table`**:**`border`**、表头底 **`#e2e8f0`**、hover **`#f1f5f9`**,首列(商品)左对齐、数值列 **`is-right`**。订单编号链式按钮 **`order-no-link`**(**`#059669`**)。操作列:**「查看/运营」`btn-op-item primary` + View**,**「分配配送员」`btn-op-item` + Bicycle**,**「审核取消」`btn-op-item op-warn` + DocumentChecked**;**`min-width`** 需容纳多枚操作钮。**订单详情(查看/运营)** **`order-detail-dialog`** + **`modal-class="order-module-dialog-mask"`**:**`el-dialog__body` `padding: 0`**,内容区 **`dialog-scroll order-detail-dialog-scroll`** 内纵向滚动;顶部 **`view-hero order-view-hero`**(**72×72** **`order-view-hero-icon` + ShoppingBag + **`view-hero-main`** 标题 / **`view-hero-tags`** / **`view-hero-meta`**),各 Tab 下区块为 **`view-section`** + **`h3`** + **`view-kv-grid` / `view-kv` / `view-multiline`**(与 **参数详情 / 询价查看** 同构,不再用 **`el-descriptions` border** 作为主信息区);底栏 **`dialog-footer`**,**关闭** **`btn-ghost` + Close**。**`el-dialog__header`** 薄荷渐变、**`el-dialog__footer`** **`#f8fafc`**;主操作 **`btn-main`**,**取消订单** **`type="danger"`**。**分配 / 审核取消 / 取消订单 / 调整 / 价格版本明细** 等辅弹窗 **`order-module-dialog`** + 同款 **`modal-class`** 与 **36px / 8px** 底栏规则(与询价大弹窗同构;无模糊遮罩)。 +15. **商家结算管理**(历史路由 **`#/order/supplier-settlement`**,页面已裁剪):页头 **`page-desc`**;**`region-tab-strip` + `region-tab-btn`**(与询价区域切换同构);**`el-tabs` `settlement-page-tabs`**(Mint 指示条);各 Tab 内 **`section.panel`**:**`panel-top`**(**`title-ic` + Wallet / Document**)、**`filter-block`**(**三列 `filter-grid`**、**`filter-actions-row`** + **`btn-ghost` / `btn-main`** + Refresh / Search、关键词 **Enter**);**待结算** 下 **`settlement-stats`**(**`stat-card` / `stat-card--accent`**);**`list-toolbar-row` + `toolbar-actions`**;主表 **`settlement-pending-main-table settlement-root-table list-table`**;**结算单记录** 主表 **`settlement-sheets-main-table settlement-root-table list-table`**;分页 **`list-pager-bar`**。**树形多层级**(对齐 **`fresh-mint-green.html` §树形列表**):待结算订单行展开为 **子列表**(**`settlement-tree-inner`** 左 **`3px #10b981`** + **`settlement-tree-label`** + **`settlement-child-table`** **`border`**、表头 **`#e2e8f0`**);结算单记录为 **结算单 → 订单 → 商品** 三层:**L1** **`settlement-tree-l1`** 内嵌 **`settlement-mid-table`**;**L2** **`settlement-tree-l2`**(琥珀左边线 + 浅黄渐变,对应 demo **`tree-nested-deep`**)+ **`settlement-line-table`**(孙表头 **`#fef3c7`** / 字 **`#92400e`**);**`expanded-cell`** 主层 **`#f8fafc`**、订单下商品层 **`#fffbeb`**。操作列:**详情 `btn-op-item primary` + View**,**编辑 `btn-op-item op-warn` + Edit**。业务弹窗 **`order-module-dialog`** + **`order-module-dialog-mask`**;本页另附 **非 scoped** 的 **`order-module-dialog`** 规则,避免未加载订单页 chunk 时弹窗无样式。订单号链式 **`settlement-order-no-link`**。 +16. **客户结算管理**(历史路由 **`#/order/client-settlement`**,页面已裁剪):与 **商家结算管理** 同构应用 Fresh Mint 壳层与交互(页头 `page-desc`、`region-tab-strip`、`settlement-page-tabs`、`panel`、`filter-block`、`list-toolbar-row`、`list-pager-bar`、`order-module-dialog`)。业务语义改为客户维度(客户筛选、客户结算状态、`client_settlement` 凭证业务类型、同客户约束)。树形多层级同样按 **结算单 → 订单 → 商品** 实现,沿用 **`settlement-tree-inner / settlement-tree-l1 / settlement-tree-l2`** 视觉分层:L1 薄荷左边线 + 灰蓝子表头,L2 琥珀左边线 + 浅黄渐变 + 孙表琥珀表头;主层与深层 expanded cell 分别使用 **`#f8fafc`** 与 **`#fffbeb`**,保证多层树形在客户结算页与标准 demo 统一。 17. **配送列表**(`#/delivery/delivery`):改造为 Fresh Mint 列表壳层——页头 **`page-head` + `page-desc`**,主体 **`section.panel`** + **`panel-top`**(`title-ic` + `Van`),筛选区升级为 **`filter-block` + `search-form` + 三列 `filter-grid` + `filter-actions-row`**(`btn-ghost` / `btn-main` + Refresh/Search,关键词支持 Enter 查询),列表与分页收口到 **`list-body list-wrap` + `list-table` + `list-pager-bar`**。配送单展开子表按树形子列表规范对齐:展开层 **`delivery-tree-inner`**(左侧薄荷 3px 线)+ 标题 **`delivery-tree-label`** + 子表滚动容器,expanded cell 背景与边框对齐全局列表规范。操作列统一为 **`btn-op-item primary` + View**;详情弹窗接入 **`order-module-dialog` + `order-module-dialog-mask`**(薄荷渐变头、无遮罩模糊)以保持订单/结算/配送弹窗视觉一致。 18. **时效预警**(`#/delivery/delivery-warning`):改造为与配送列表同构的 Fresh Mint 壳层——页头 **`page-head` + `page-desc`**,主体 **`panel` + `panel-top`**(`title-ic` + `Bell`),筛选区统一 **`filter-block` + `search-form` + `filter-grid` + `filter-actions-row`**(`btn-ghost` / `btn-main` + Refresh/Search),列表区统一 **`list-body list-wrap` + `list-table` + `list-pager-bar`**。预警行展开商品明细同样采用树形子列表样式:**`delivery-tree-inner`** 左 3px 薄荷线、**`delivery-tree-label`** 标题、子表滚动容器与 expanded cell 背景边框。操作列统一按钮语义:**查看** `btn-op-item primary + View`,**处理** `btn-op-item op-warn + Finished`。处理弹窗与详情弹窗接入 **`order-module-dialog` + `order-module-dialog-mask`**,与订单/结算/配送页面维持一致的薄荷渐变头和无遮罩模糊策略。 19. **系统设置**(`#/system/system-config`):将原单块白底表单改造为 Fresh Mint 配置页结构——页头 **`page-head` + `page-desc`**,配置项拆分为两张 **`settings-panel`**(订单设置 / 客服设置),每块使用 **`panel-top` + `title-ic`**(`Setting` / `PhoneFilled`)与 **`panel-body`**。表单统一为 **`label-position="top"`** 与 Mint 输入态(11px label、8px 圆角、`#d9e8e2` 边),采用 **`form-grid`**(桌面两列、窄屏一列)排版;数字输入配单位说明与 hint 文案。保存区使用统一主按钮 **`btn-main`**(`Check` 图标 + 36px 高 + 薄荷色链),整体与已改造业务页面的卡片、标题、按钮体系一致。 20. **接口日志**(`#/system/api-request-log`):改造为 Fresh Mint 列表页模板——页头 **`page-head` + `page-desc`**,主体 **`panel` + `panel-top`**(`title-ic` + `Document`),筛选区统一为 **`filter-block` + `search-form` + 四列 `filter-grid` + `filter-actions-row`**(`btn-ghost` / `btn-main` + Refresh/Search,关键词支持 Enter 查询);日志表格与分页统一到 **`list-body list-wrap` + `list-table` + `list-pager-bar`**。操作列“详情”改为 **`btn-op-item primary + View`**。请求详情弹窗接入 **`order-module-dialog` + `order-module-dialog-mask`**(薄荷渐变头、无遮罩模糊、统一 footer),请求体/响应体保留 `pre` 滚动展示能力。 -21. **全局壳层回归(2026-04-17)**:针对 `git restore` 导致的样式回退,补齐 `web-admin` Fresh Mint 基线:`index.scss` 重新引入 `fresh-mint-tokens.scss`;`app.scss` 回收全局 token 映射(`--theme-color`、`--default-bg-color`、`--default-box-color`、`--art-card-border`)、统一页头/面板/侧栏骨架;`views/index/style.scss` 恢复内容区 **`24px`** 内边距与「壳层不包二次卡片」策略;`el-ui.scss` 重设 Element 主题变量、弹窗头体脚间距、表格与主按钮 Mint 色链;`#/dashboard/console` 恢复 **page-head + panel/list-body/list-table** 结构与统计卡的 Mint 视觉层级。 -22. **第二轮批量回归(2026-04-17)**:新增跨页面全局兜底(`app.scss` + `el-ui.scss`)用于统一 **筛选区 / 工具栏 / 列表体 / 分页条 / 表单控件 / 主次按钮 / 行内操作按钮**。约定类(`filter-block`、`filter-grid`、`filter-actions-row`、`list-toolbar-row`、`list-body`、`list-wrap`、`list-pager-bar`、`btn-main`、`btn-ghost`、`btn-op-item`)在 `page-container` 与 `console-container` 下自动应用 Mint 视觉,降低逐页重复样式。同步修正历史弹窗组件 `#/base/user`(`user-dialog`)与 `#/system/menu`(`menu-dialog`)底栏按钮与表单布局,使其对齐 **36px/8px** 按钮与顶部标签表单规范。 -23. **第三轮业务页收口(2026-04-17)**:针对运营高频页面做残留旧色清理与语义色统一:`#/delivery/delivery`、`#/delivery/delivery-warning`、`#/system/system-config`、`#/order/order` 中遗留的旧角色色(`#1989fa/#07c160/#ff976a/#ee0a24`)已替换为 Fresh Mint 语义色链(主色 `#10b981/#059669`、警告 `#d97706`、危险 `#dc2626`),覆盖 section 左边线、金额强调、Tab 激活态、地址卡片强调边、时间轴节点、风险提示等关键视觉点,避免后台页面出现“蓝/绿/橙/红混搭”的历史风格。 +21. **全局壳层回归(2026-04-17)**:针对 `git restore` 导致的样式回退,补齐 `web-admin` Fresh Mint 基线:`index.scss` 重新引入 `fresh-mint-tokens.scss`;`app.scss` 回收全局 token 映射(`--theme-color`、`--default-bg-color`、`--default-box-color`、`--art-card-border`)、统一页头/面板/侧栏骨架;`views/index/style.scss` 恢复内容区 **`24px`** 内边距与「壳层不包二次卡片」策略;`el-ui.scss` 重设 Element 主题变量、弹窗头体脚间距、表格与主按钮 Mint 色链;`#/demo/console` 恢复 **page-head + panel/list-body/list-table** 结构与统计卡的 Mint 视觉层级。 +22. **第二轮批量回归(2026-04-17)**:新增跨页面全局兜底(`app.scss` + `el-ui.scss`)用于统一 **筛选区 / 工具栏 / 列表体 / 分页条 / 表单控件 / 主次按钮 / 行内操作按钮**。约定类(`filter-block`、`filter-grid`、`filter-actions-row`、`list-toolbar-row`、`list-body`、`list-wrap`、`list-pager-bar`、`btn-main`、`btn-ghost`、`btn-op-item`)在 `page-container` 与 `console-container` 下自动应用 Mint 视觉,降低逐页重复样式。同步修正历史弹窗组件 `#/demo/user`(`user-dialog`)与 `#/system/menu`(`menu-dialog`)底栏按钮与表单布局,使其对齐 **36px/8px** 按钮与顶部标签表单规范。 +23. **第三轮业务页收口(2026-04-17)**:针对运营高频页面做残留旧色清理与语义色统一:`#/delivery/delivery`、`#/delivery/delivery-warning`、`#/system/system-config`、`#/demo/order` 中遗留的旧角色色(`#1989fa/#07c160/#ff976a/#ee0a24`)已替换为 Fresh Mint 语义色链(主色 `#10b981/#059669`、警告 `#d97706`、危险 `#dc2626`),覆盖 section 左边线、金额强调、Tab 激活态、地址卡片强调边、时间轴节点、风险提示等关键视觉点,避免后台页面出现“蓝/绿/橙/红混搭”的历史风格。 24. **第四轮一致性细节(2026-04-17)**:补充跨页面细节兜底:`app.scss` 新增 `page-container/console-container` 下通用 `.pagination`(贴表格的分页条样式)与 `.search-bar`(白底卡片化);`el-ui.scss` 新增弹窗 footer 按钮统一规则(**36px 高、8px 圆角、600 字重**,非主按钮中性描边)及 footer 内图标右间距 **6px**,用于收敛历史页面中未显式加 `btn-main/btn-ghost` 的弹窗底栏视觉差异。 25. **第五轮验收清单(2026-04-17)**:对 `web-admin/src/views/system` 全量页执行旧色值巡检并完成批量替换,遗留角色色 `#1989fa/#07c160/#ff976a/#ee0a24` 已在 system 目录清零;重点修正页面包括 `price-list`、`client/supplier-settlement`、`category`、`order-adjust`、`quotation`、`user/detail`、`statistics`、`quotation/compare`、`price-list/version`、`refund`、`inquiry-quotation`、`invoice`、`settlement`、`product`、`inquiry`。同时补齐角色管理弹窗 (`role-edit-dialog`、`role-permission-dialog`) footer 按钮语义类(`btn-ghost` / `btn-main`),与全局弹窗底栏规范保持一致。 26. **第六轮结构统一(进行中,2026-04-17)**:开始将“视觉已统一但结构仍旧”的页面改为标准模板。首批完成 `#/system/review`、`#/system/settlement`、`#/system/invoice`:统一为 **`page-head + panel + filter-block/filter-grid/filter-actions-row + list-body/list-wrap + list-pager-bar`**,并将操作列按钮改为 **`btn-op-item`** 体系、弹窗 footer 改为 **`btn-ghost / btn-main`** 语义按钮,减少历史 `search-bar + inline form + link 按钮` 结构差异。 @@ -645,15 +647,15 @@ 33. **第六轮结构统一(第八批,2026-04-17)**:继续按 `#/delivery/delivery-demo` 模板推进 `#/system/api-request-log` 与 `#/system/user`:统一为 `page-head + panel + panel-top + filter-block + filter-grid + filter-actions-row + list-toolbar + list-body/list-wrap + list-pager-bar`;操作列链接按钮改为 `btn-op-item`;新增/编辑弹窗 footer 统一 `btn-ghost / btn-main`,并清理页面内 `search-bar/toolbar/pagination` 的重复 scoped 壳层样式。 34. **第六轮结构统一(第九批,2026-04-17)**:完成 `#/system/statistics` 的旧 `filter-bar/card` 体系替换,整体对齐 `#/delivery/delivery-demo`:新增 `page-head` 与 `panel` 分段结构,筛选区切为 `filter-block + filter-grid + filter-actions-row`(补齐“搜索/重置”动作),运营概览改为统一统计卡网格(`stats-grid`),图表与明细改为 `panel + panel-top + list-body/list-wrap + list-table` 组合,显著降低该页与其他业务页的视觉割裂感。 35. **第六轮结构统一(第十批,2026-04-17)**:推进 `#/system/product`、`#/system/inquiry`、`#/system/supplier-settlement`、`#/system/client-settlement` 对齐 `#/delivery/delivery-demo`。本批聚焦“高流量复杂页”的外层一致性:统一页头说明(`page-head/page-desc`)、筛选动作文案(“搜索/重置”)、主次按钮语义类(`btn-main/btn-ghost`)、列表操作按钮(`btn-op-item`)、分页条(`list-pager-bar`)与弹窗 footer 按钮语义。`product` 与 `inquiry` 同步补齐 `panel/panel-top/list-toolbar` 骨架;`supplier/client-settlement` 保持双 Tab 与树形明细业务结构不变,仅做 Mint 交互壳层与动作区收口,确保改造不触及结算逻辑。 -36. **商品分类对齐区域管理流程(2026-04-17)**:`#/base/category` 参考 `#/system/region` 的改造方法完成收口:列表工具栏补齐 **`list-toolbar-selected + sel-num`**,并保持“首按钮主色、其余中性描边”(含批量导入);表格列头/列值统一居中对齐,状态标签语义统一为 **启用=success、禁用=info**;操作列去除 link 形态,统一 **`btn-op-item`** 体系;新增/编辑弹窗统一 **`label-position="top"`** 与两列排布,footer 统一 **`btn-ghost/btn-main + Close/Check`** 图标,确保与区域管理及全局 Fresh Mint 规范一致。 -37. **商品单位按同构流程收口(2026-04-17)**:`#/base/unit` 参照 `#/system/region` 与 `#/base/category` 的改造路径统一壳层与交互细节:工具栏增加 **`list-toolbar-selected + sel-num`**,并保持“首按钮主色、其余中性描边”(含批量导入);状态标签语义统一为 **启用=success、禁用=info**;操作列固定右侧并预留宽度(≥260);新增/编辑弹窗改为 **`label-position="top"`** 两列布局,footer 统一 **`btn-ghost/btn-main + Close/Check`** 图标规范。 -38. **商品管理同构改造(2026-04-17)**:`#/base/product` 对齐 `fresh-mint-green` 与区域/分类/单位页面规范:页头改为 **`page-head + h1 + page-desc`**;筛选区改为 **`filter-form + filter-grid + filter-actions-row`**(非 inline);列表工具栏合并为单行 **`list-toolbar-row`**(含 **`list-toolbar-selected + sel-num`** 与主次按钮);表格列头/列值统一居中,状态标签语义为 **启用=success、禁用=info**;操作列改为 **`btn-op-item`**(去 link 形态)并固定右侧预留宽度;新增/编辑与选择参数弹窗 footer 统一 **`btn-ghost/btn-main + Close/Check`**,参数选择表格去 `border` 并改为 Mint 容器边框与透明检索栏。 +36. **商品分类对齐区域管理流程(2026-04-17)**:`#/demo/category` 参考 `#/system/region` 的改造方法完成收口:列表工具栏补齐 **`list-toolbar-selected + sel-num`**,并保持“首按钮主色、其余中性描边”(含批量导入);表格列头/列值统一居中对齐,状态标签语义统一为 **启用=success、禁用=info**;操作列去除 link 形态,统一 **`btn-op-item`** 体系;新增/编辑弹窗统一 **`label-position="top"`** 与两列排布,footer 统一 **`btn-ghost/btn-main + Close/Check`** 图标,确保与区域管理及全局 Fresh Mint 规范一致。 +37. **商品单位按同构流程收口(2026-04-17)**:`#/demo/unit` 参照 `#/system/region` 与 `#/demo/category` 的改造路径统一壳层与交互细节:工具栏增加 **`list-toolbar-selected + sel-num`**,并保持“首按钮主色、其余中性描边”(含批量导入);状态标签语义统一为 **启用=success、禁用=info**;操作列固定右侧并预留宽度(≥260);新增/编辑弹窗改为 **`label-position="top"`** 两列布局,footer 统一 **`btn-ghost/btn-main + Close/Check`** 图标规范。 +38. **商品管理同构改造(2026-04-17)**:`#/demo/product` 对齐 `fresh-mint-green` 与区域/分类/单位页面规范:页头改为 **`page-head + h1 + page-desc`**;筛选区改为 **`filter-form + filter-grid + filter-actions-row`**(非 inline);列表工具栏合并为单行 **`list-toolbar-row`**(含 **`list-toolbar-selected + sel-num`** 与主次按钮);表格列头/列值统一居中,状态标签语义为 **启用=success、禁用=info**;操作列改为 **`btn-op-item`**(去 link 形态)并固定右侧预留宽度;新增/编辑与选择参数弹窗 footer 统一 **`btn-ghost/btn-main + Close/Check`**,参数选择表格去 `border` 并改为 Mint 容器边框与透明检索栏。 39. **树形字段统一路径展示(全局,2026-04-17)**:凡树形字段(商品分类等)统一执行“**下拉展示本级、选中回显全路径、列表展示全路径**”规则:`el-tree-select` 展开态节点文案保留本级名称;选中后输入框通过 `#label` 插槽显示 `根 / 子 / 孙` 全路径;表格/卡片中的分类字段同样优先展示全路径(缺路径时回退接口字段)。实现统一复用 `web-admin/src/utils/tree-path.ts`(`buildTreePathLabelFromFlat`、`findTreeSelectShortLabel`),避免各页面重复拼接逻辑。 -40. **询价报价管理二轮收口(2026-04-17)**:`#/business/inquiry-quotation` 按 Fresh Mint 模板完成结构升级:补齐 `page-head + panel + panel-top`,区域切换统一为 `region-tab-strip/region-tab-btn`;询价与历史报价列表改为 `list-body/list-wrap + list-table` 组合;历史报价筛选改为 `filter-block + history-filter-grid + filter-actions-row`(重置/搜索按钮采用 `btn-ghost/btn-main` + 图标);操作列从 link 统一为 `btn-op-item`(首动作主色),并将历史列表分页条对齐为 `list-pager-bar`;关键弹窗 footer 同步 `btn-ghost/btn-main + Close/Check`,保持与已改造基础数据页同一视觉语言。 -41. **询价报价管理二轮精修(2026-04-17)**:继续对 `#/business/inquiry-quotation` 弹窗与内嵌选品区细节收口:`新增/编辑询价`、`询价详情`、`报价明细`、`调价确认` 统一接入 `inquiry-quotation-dialog` + `inquiry-quotation-dialog-mask`(header 薄荷渐变、footer 浅底、无遮罩模糊);弹窗 footer 按全站规范使用 `btn-ghost/btn-main` 与 `Close/Check` 图标;选品区(左树/右列表/已选区/卡片选中态)统一替换旧灰蓝边框为 Mint 语义边框与主色阴影;历史报价操作列保持固定右侧并使用 `btn-op-item primary`,补齐“弹窗内按钮层级、间距密度、操作列语义”与基础数据页一致性。 -42. **工作台快捷菜单(2026-04-18)**:曾实现 **`shortcuts-grid` + `shortcut-tile`**(`ArtSvgIcon` + **`meta.icon`**);**(2026-04-18 后续)已移除**——`#/dashboard/console` **不再展示**独立快捷菜单区,常用入口以侧栏导航为准;**§4.2** 工作台描述已同步。 -43. **工作台近30日折线(2026-04-18)**:`#/dashboard/console` 在数据概览下增加 **`trend-charts-panel`**:双 **`ArtLineChart`**(营业额元 / 订单数笔),接口 **`fetchOrderTrend` → `/web/admin/statistics/orders/trend?days=30`**(复用 **`statistics_crud.get_order_trend`**);**§4.2** 已补充。 -44. **工作台今日各区域柱状(2026-04-18)**:`#/dashboard/console` 在折线图下增加 **`region-today-panel`**:**`RegionTodayDualBarChart`** 分组柱 + 双 **Y** 轴(左营业额元、右订单笔),接口 **`fetchTodayOrdersByRegion` → `/web/admin/statistics/orders/today-by-region`**(**`statistics_crud.get_today_order_stats_by_region`**:启用区域全量,无单为 0);**§4.2**、实现清单 **§7** 已同步。 +40. **询价报价管理二轮收口(2026-04-17)**(历史页 **`#/business/inquiry-quotation`**):补齐 `page-head + panel + panel-top`,区域切换统一为 `region-tab-strip/region-tab-btn`;询价与历史报价列表改为 `list-body/list-wrap + list-table` 组合;历史报价筛选改为 `filter-block + history-filter-grid + filter-actions-row`(重置/搜索按钮采用 `btn-ghost/btn-main` + 图标);操作列从 link 统一为 `btn-op-item`(首动作主色),并将历史列表分页条对齐为 `list-pager-bar`;关键弹窗 footer 同步 `btn-ghost/btn-main + Close/Check`,保持与已改造基础数据页同一视觉语言。 +41. **询价报价管理二轮精修(2026-04-17)**(历史页 **`#/business/inquiry-quotation`**):继续对 **`新增/编辑询价`**、**`询价详情`**、**`报价明细`**、**`调价确认`** 统一接入 `inquiry-quotation-dialog` + `inquiry-quotation-dialog-mask`(header 薄荷渐变、footer 浅底、无遮罩模糊);弹窗 footer 按全站规范使用 `btn-ghost/btn-main` 与 `Close/Check` 图标;选品区(左树/右列表/已选区/卡片选中态)统一替换旧灰蓝边框为 Mint 语义边框与主色阴影;历史报价操作列保持固定右侧并使用 `btn-op-item primary`,补齐“弹窗内按钮层级、间距密度、操作列语义”与基础数据页一致性。 +42. **工作台快捷菜单(2026-04-18)**:曾实现 **`shortcuts-grid` + `shortcut-tile`**(`ArtSvgIcon` + **`meta.icon`**);**(2026-04-18 后续)已移除**——`#/demo/console` **不再展示**独立快捷菜单区,常用入口以侧栏导航为准;**§4.2** 工作台描述已同步。 +43. **工作台近30日折线(2026-04-18)**:`#/demo/console` 在数据概览下增加 **`trend-charts-panel`**:双 **`ArtLineChart`**(营业额元 / 订单数笔),接口 **`fetchOrderTrend` → `/web/admin/statistics/orders/trend?days=30`**(复用 **`statistics_crud.get_order_trend`**);**§4.2** 已补充。 +44. **工作台今日各区域柱状(2026-04-18)**:`#/demo/console` 在折线图下增加 **`region-today-panel`**:**`RegionTodayDualBarChart`** 分组柱 + 双 **Y** 轴(左营业额元、右订单笔),接口 **`fetchTodayOrdersByRegion` → `/web/admin/statistics/orders/today-by-region`**(**`statistics_crud.get_today_order_stats_by_region`**:启用区域全量,无单为 0);**§4.2**、实现清单 **§7** 已同步。 45. **工作台今日客户柱状与标题竖条(2026-04-18)**:**`region-today-split`** 右列「今日下单客户」:**`fetchTodayOrdersByClient` → `/web/admin/statistics/orders/today-by-client`**(**`get_today_order_stats_by_client`**:今日下单金额合计 + **`biz_order_item.quantity`** 合计,金额降序至多 30 条);**`RegionTodayDualBarChart`** 支持系列名/空文案等 props;**`App.vue`** **`panelTitleBarTextRules`** 增加 **近30日趋势**、**今日各区域** 以使用 **`title-bar-heading`** 绿色竖条。 --- diff --git a/web-admin/index.html b/web-admin/index.html index c15f2e42..88b840ce 100644 --- a/web-admin/index.html +++ b/web-admin/index.html @@ -1,12 +1,12 @@ - 中科兴皖一体化门户 - 管理后台 + 陇耘鲜达 - 运营管理后台 diff --git a/web-admin/package.json b/web-admin/package.json index f2f04dd8..aef07e51 100644 --- a/web-admin/package.json +++ b/web-admin/package.json @@ -1,7 +1,7 @@ { - "name": "zkxw-web-admin", + "name": "sanqiang-web-admin", "version": "1.0.0", - "description": "中科兴皖一体化门户管理后台", + "description": "陇耘鲜达运营管理后台", "type": "module", "engines": { "node": ">=20.19.0", diff --git a/web-admin/scripts/clean-dev.ts b/web-admin/scripts/clean-dev.ts index cc0b9bce..e0e49ae9 100644 --- a/web-admin/scripts/clean-dev.ts +++ b/web-admin/scripts/clean-dev.ts @@ -1,627 +1,44 @@ -// scripts/clean-dev.ts +/** + * 可选本地维护脚本(`pnpm run clean:dev`) + * + * 说明:历史上本脚本会批量删除模板演示资源并重写 `router/modules`、README 等, + * 与当前 zkxw 已裁剪的 `web-admin` 冲突。**已移除所有破坏性步骤**。 + * + * 现仅支持:在确认后清空 `src/mock/upgrade/changeLog.ts` 中的演示升级记录(可选)。 + */ import fs from 'fs/promises' import path from 'path' -// 现代化颜色主题 const theme = { - // 基础颜色 reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', - - // 前景色 - primary: '\x1b[38;5;75m', // 亮蓝色 - success: '\x1b[38;5;82m', // 亮绿色 - warning: '\x1b[38;5;220m', // 亮黄色 - error: '\x1b[38;5;196m', // 亮红色 - info: '\x1b[38;5;159m', // 青色 - purple: '\x1b[38;5;141m', // 紫色 - orange: '\x1b[38;5;208m', // 橙色 - gray: '\x1b[38;5;245m', // 灰色 - white: '\x1b[38;5;255m', // 白色 - - // 背景色 - bgDark: '\x1b[48;5;235m', // 深灰背景 - bgBlue: '\x1b[48;5;24m', // 蓝色背景 - bgGreen: '\x1b[48;5;22m', // 绿色背景 - bgRed: '\x1b[48;5;52m' // 红色背景 + primary: '\x1b[38;5;75m', + success: '\x1b[38;5;82m', + warning: '\x1b[38;5;220m', + error: '\x1b[38;5;196m', + gray: '\x1b[38;5;245m', + white: '\x1b[38;5;255m' } -// 现代化图标集 -const icons = { - rocket: '🚀', - fire: '🔥', - star: '⭐', - gem: '💎', - crown: '👑', - magic: '✨', - warning: '⚠️', - success: '✅', - error: '❌', - info: 'ℹ️', - folder: '📁', - file: '📄', - image: '🖼️', - code: '💻', - data: '📊', - globe: '🌐', - map: '🗺️', - chat: '💬', - bolt: '⚡', - shield: '🛡️', - key: '🔑', - link: '🔗', - clean: '🧹', - trash: '🗑️', - check: '✓', - cross: '✗', - arrow: '→', - loading: '⏳' -} - -// 格式化工具 const fmt = { - title: (text: string) => `${theme.bold}${theme.primary}${text}${theme.reset}`, - subtitle: (text: string) => `${theme.purple}${text}${theme.reset}`, - success: (text: string) => `${theme.success}${text}${theme.reset}`, - error: (text: string) => `${theme.error}${text}${theme.reset}`, - warning: (text: string) => `${theme.warning}${text}${theme.reset}`, - info: (text: string) => `${theme.info}${text}${theme.reset}`, - highlight: (text: string) => `${theme.bold}${theme.white}${text}${theme.reset}`, - dim: (text: string) => `${theme.dim}${theme.gray}${text}${theme.reset}`, - orange: (text: string) => `${theme.orange}${text}${theme.reset}`, - - // 带背景的文本 - badge: (text: string, bg: string = theme.bgBlue) => - `${bg}${theme.white}${theme.bold} ${text} ${theme.reset}`, - - // 渐变效果模拟 - gradient: (text: string) => { - const colors = ['\x1b[38;5;75m', '\x1b[38;5;81m', '\x1b[38;5;87m', '\x1b[38;5;159m'] - const chars = text.split('') - return chars.map((char, i) => `${colors[i % colors.length]}${char}`).join('') + theme.reset - } + title: (t: string) => `${theme.bold}${theme.primary}${t}${theme.reset}`, + success: (t: string) => `${theme.success}${t}${theme.reset}`, + error: (t: string) => `${theme.error}${t}${theme.reset}`, + warning: (t: string) => `${theme.warning}${t}${theme.reset}`, + highlight: (t: string) => `${theme.bold}${theme.white}${t}${theme.reset}`, + dim: (t: string) => `${theme.dim}${theme.gray}${t}${theme.reset}` } -// 创建现代化标题横幅 -function createModernBanner() { - console.log() - console.log( - fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗') - ) - console.log( - fmt.gradient(' ║ ║') - ) - console.log( - ` ║ ${icons.rocket} ${fmt.title('ART DESIGN PRO')} ${fmt.subtitle('· 代码精简程序')} ${icons.magic} ║` - ) - console.log( - ` ║ ${fmt.dim('为项目移除演示数据,快速切换至开发模式')} ║` - ) - console.log( - fmt.gradient(' ║ ║') - ) - console.log( - fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝') - ) - console.log() -} - -// 创建分割线 -function createDivider(char = '─', color = theme.primary) { - console.log(`${color}${' ' + char.repeat(66)}${theme.reset}`) -} - -// 创建卡片样式容器 -function createCard(title: string, content: string[]) { - console.log(` ${fmt.badge('', theme.bgBlue)} ${fmt.title(title)}`) - console.log() - content.forEach((line) => { - console.log(` ${line}`) - }) - console.log() -} - -// 进度条动画 -function createProgressBar(current: number, total: number, text: string, width = 40) { - const percentage = Math.round((current / total) * 100) - const filled = Math.round((current / total) * width) - const empty = width - filled - - const filledBar = '█'.repeat(filled) - const emptyBar = '░'.repeat(empty) - - process.stdout.write( - `\r ${fmt.info('进度')} [${theme.success}${filledBar}${theme.gray}${emptyBar}${theme.reset}] ${fmt.highlight(percentage + '%')})}` - ) - - if (current === total) { - console.log() - } -} - -// 统计信息 -const stats = { - deletedFiles: 0, - deletedPaths: 0, - failedPaths: 0, - startTime: Date.now(), - totalFiles: 0 -} - -// 清理目标 -const targets = [ - 'README.md', - 'README.zh-CN.md', - 'CHANGELOG.md', - 'CHANGELOG.zh-CN.md', - 'src/views/change', - 'src/views/safeguard', - 'src/views/article', - 'src/views/examples', - 'src/views/system/nested', - 'src/views/widgets', - 'src/views/template', - 'src/views/dashboard/analysis', - 'src/views/dashboard/ecommerce', - 'src/mock/json', - 'src/mock/temp/articleList.ts', - 'src/mock/temp/commentDetail.ts', - 'src/mock/temp/commentList.ts', - 'src/assets/images/cover', - 'src/assets/images/safeguard', - 'src/assets/images/3d', - 'src/components/core/charts/art-map-chart', - 'src/components/business/comment-widget' -] - -// 递归统计文件数量 -async function countFiles(targetPath: string): Promise { - const fullPath = path.resolve(process.cwd(), targetPath) - - try { - const stat = await fs.stat(fullPath) - - if (stat.isFile()) { - return 1 - } else if (stat.isDirectory()) { - const entries = await fs.readdir(fullPath) - let count = 0 - - for (const entry of entries) { - const entryPath = path.join(targetPath, entry) - count += await countFiles(entryPath) - } - - return count - } - } catch { - return 0 - } - - return 0 -} - -// 统计所有目标的文件数量 -async function countAllFiles(): Promise { - let totalCount = 0 - - for (const target of targets) { - const count = await countFiles(target) - totalCount += count - } - - return totalCount -} - -// 删除文件和目录 -async function remove(targetPath: string, index: number) { - const fullPath = path.resolve(process.cwd(), targetPath) - - createProgressBar(index + 1, targets.length, targetPath) - - try { - const fileCount = await countFiles(targetPath) - await fs.rm(fullPath, { recursive: true, force: true }) - stats.deletedFiles += fileCount - stats.deletedPaths++ - await new Promise((resolve) => setTimeout(resolve, 50)) - } catch (err) { - stats.failedPaths++ - console.log() - console.log(` ${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(targetPath)}`) - console.log(` ${fmt.dim('错误详情: ' + err)}`) - } -} - -// 清理路由模块 -async function cleanRouteModules() { - const modulesPath = path.resolve(process.cwd(), 'src/router/modules') - - try { - // 删除演示相关的路由模块 - const modulesToRemove = [ - 'template.ts', - 'widgets.ts', - 'examples.ts', - 'article.ts', - 'safeguard.ts', - 'help.ts' - ] - - for (const module of modulesToRemove) { - const modulePath = path.join(modulesPath, module) - try { - await fs.rm(modulePath, { force: true }) - } catch { - // 文件不存在时忽略错误 - } - } - - // 重写 dashboard.ts - 只保留 console - const dashboardContent = `import { AppRouteRecord } from '@/types/router' - -export const dashboardRoutes: AppRouteRecord = { - name: 'Dashboard', - path: '/dashboard', - component: '/index/index', - meta: { - title: 'menus.dashboard.title', - icon: 'ri:pie-chart-line', - roles: ['R_SUPER', 'R_ADMIN'] - }, - children: [ - { - path: 'console', - name: 'Console', - component: '/dashboard/console', - meta: { - title: 'menus.dashboard.console', - keepAlive: false, - fixedTab: true - } - } - ] -} -` - await fs.writeFile(path.join(modulesPath, 'dashboard.ts'), dashboardContent, 'utf-8') - - // 重写 system.ts - 移除 nested 嵌套菜单 - const systemContent = `import { AppRouteRecord } from '@/types/router' - -export const systemRoutes: AppRouteRecord = { - path: '/system', - name: 'System', - component: '/index/index', - meta: { - title: 'menus.system.title', - icon: 'ri:user-3-line', - roles: ['R_SUPER', 'R_ADMIN'] - }, - children: [ - { - path: 'user', - name: 'User', - component: '/system/user', - meta: { - title: 'menus.system.user', - keepAlive: true, - roles: ['R_SUPER', 'R_ADMIN'] - } - }, - { - path: 'role', - name: 'Role', - component: '/system/role', - meta: { - title: 'menus.system.role', - keepAlive: true, - roles: ['R_SUPER'] - } - }, - { - path: 'user-center', - name: 'UserCenter', - component: '/system/user-center', - meta: { - title: 'menus.system.userCenter', - isHide: true, - keepAlive: true, - isHideTab: true - } - }, - { - path: 'menu', - name: 'Menus', - component: '/system/menu', - meta: { - title: 'menus.system.menu', - keepAlive: true, - roles: ['R_SUPER'], - authList: [ - { title: '新增', authMark: 'add' }, - { title: '编辑', authMark: 'edit' }, - { title: '删除', authMark: 'delete' } - ] - } - } - ] -} -` - await fs.writeFile(path.join(modulesPath, 'system.ts'), systemContent, 'utf-8') - - // 重写 index.ts - 只导入保留的模块 - const indexContent = `import { AppRouteRecord } from '@/types/router' -import { dashboardRoutes } from './dashboard' -import { systemRoutes } from './system' -import { resultRoutes } from './result' -import { exceptionRoutes } from './exception' - -/** - * 导出所有模块化路由 - */ -export const routeModules: AppRouteRecord[] = [ - dashboardRoutes, - systemRoutes, - resultRoutes, - exceptionRoutes -] -` - await fs.writeFile(path.join(modulesPath, 'index.ts'), indexContent, 'utf-8') - - console.log(` ${icons.success} ${fmt.success('清理路由模块完成')}`) - } catch (err) { - console.log(` ${icons.error} ${fmt.error('清理路由模块失败')}`) - console.log(` ${fmt.dim('错误详情: ' + err)}`) - } -} - -// 清理路由别名 -async function cleanRoutesAlias() { - const routesAliasPath = path.resolve(process.cwd(), 'src/router/routesAlias.ts') - - try { - const cleanedAlias = `/** - * 公共路由别名 - # 存放系统级公共路由路径,如布局容器、登录页等 - */ -export enum RoutesAlias { - Layout = '/index/index', // 布局容器 - Login = '/auth/login' // 登录页 -} -` - - await fs.writeFile(routesAliasPath, cleanedAlias, 'utf-8') - console.log(` ${icons.success} ${fmt.success('重写路由别名配置完成')}`) - } catch (err) { - console.log(` ${icons.error} ${fmt.error('清理路由别名失败')}`) - console.log(` ${fmt.dim('错误详情: ' + err)}`) - } -} - -// 清理变更日志 -async function cleanChangeLog() { - const changeLogPath = path.resolve(process.cwd(), 'src/mock/upgrade/changeLog.ts') - - try { - const cleanedChangeLog = `import { ref } from 'vue' - -interface UpgradeLog { - version: string // 版本号 - title: string // 更新标题 - date: string // 更新日期 - detail?: string[] // 更新内容 - requireReLogin?: boolean // 是否需要重新登录 - remark?: string // 备注 -} - -export const upgradeLogList = ref([]) -` - - await fs.writeFile(changeLogPath, cleanedChangeLog, 'utf-8') - console.log(` ${icons.success} ${fmt.success('清空变更日志数据完成')}`) - } catch (err) { - console.log(` ${icons.error} ${fmt.error('清理变更日志失败')}`) - console.log(` ${fmt.dim('错误详情: ' + err)}`) - } -} - -// 清理语言文件 -async function cleanLanguageFiles() { - const languageFiles = [ - { path: 'src/locales/langs/zh.json', name: '中文语言文件' }, - { path: 'src/locales/langs/en.json', name: '英文语言文件' } - ] - - for (const { path: langPath, name } of languageFiles) { - try { - const fullPath = path.resolve(process.cwd(), langPath) - const content = await fs.readFile(fullPath, 'utf-8') - const langData = JSON.parse(content) - - const menusToRemove = [ - 'widgets', - 'template', - 'article', - 'examples', - 'safeguard', - 'plan', - 'help' - ] - - if (langData.menus) { - menusToRemove.forEach((menuKey) => { - if (langData.menus[menuKey]) { - delete langData.menus[menuKey] - } - }) - - if (langData.menus.dashboard) { - if (langData.menus.dashboard.analysis) { - delete langData.menus.dashboard.analysis - } - if (langData.menus.dashboard.ecommerce) { - delete langData.menus.dashboard.ecommerce - } - } - - if (langData.menus.system) { - const systemKeysToRemove = [ - 'nested', - 'menu1', - 'menu2', - 'menu21', - 'menu3', - 'menu31', - 'menu32', - 'menu321' - ] - systemKeysToRemove.forEach((key) => { - if (langData.menus.system[key]) { - delete langData.menus.system[key] - } - }) - } - } - - await fs.writeFile(fullPath, JSON.stringify(langData, null, 2), 'utf-8') - console.log(` ${icons.success} ${fmt.success(`清理${name}完成`)}`) - } catch (err) { - console.log(` ${icons.error} ${fmt.error(`清理${name}失败`)}`) - console.log(` ${fmt.dim('错误详情: ' + err)}`) - } - } -} - -// 清理快速入口组件 -async function cleanFastEnterComponent() { - const fastEnterPath = path.resolve(process.cwd(), 'src/config/fastEnter.ts') - - try { - const cleanedFastEnter = `/** - * 快速入口配置 - * 包含:应用列表、快速链接等配置 - */ -import { WEB_LINKS } from '@/utils/constants' -import type { FastEnterConfig } from '@/types/config' - -const fastEnterConfig: FastEnterConfig = { - // 显示条件(屏幕宽度) - minWidth: 1200, - // 应用列表 - applications: [ - { - name: '工作台', - description: '系统概览与数据统计', - icon: 'ri:pie-chart-line', - iconColor: '#377dff', - enabled: true, - order: 1, - routeName: 'Console' - }, - { - name: '官方文档', - description: '使用指南与开发文档', - icon: 'ri:bill-line', - iconColor: '#ffb100', - enabled: true, - order: 2, - link: WEB_LINKS.DOCS - }, - { - name: '技术支持', - description: '技术支持与问题反馈', - icon: 'ri:user-location-line', - iconColor: '#ff6b6b', - enabled: true, - order: 3, - link: WEB_LINKS.COMMUNITY - }, - { - name: '哔哩哔哩', - description: '技术分享与交流', - icon: 'ri:bilibili-line', - iconColor: '#FB7299', - enabled: true, - order: 4, - link: WEB_LINKS.BILIBILI - } - ], - // 快速链接 - quickLinks: [ - { - name: '登录', - enabled: true, - order: 1, - routeName: 'Login' - }, - { - name: '注册', - enabled: true, - order: 2, - routeName: 'Register' - }, - { - name: '忘记密码', - enabled: true, - order: 3, - routeName: 'ForgetPassword' - }, - { - name: '个人中心', - enabled: true, - order: 4, - routeName: 'UserCenter' - } - ] -} - -export default Object.freeze(fastEnterConfig) -` - - await fs.writeFile(fastEnterPath, cleanedFastEnter, 'utf-8') - console.log(` ${icons.success} ${fmt.success('清理快速入口配置完成')}`) - } catch (err) { - console.log(` ${icons.error} ${fmt.error('清理快速入口配置失败')}`) - console.log(` ${fmt.dim('错误详情: ' + err)}`) - } -} - -// 更新菜单接口 -async function updateMenuApi() { - const apiPath = path.resolve(process.cwd(), 'src/api/system-manage.ts') - - try { - const content = await fs.readFile(apiPath, 'utf-8') - const updatedContent = content.replace( - "url: '/api/v3/system/menus'", - "url: '/api/v3/system/menus/simple'" - ) - - await fs.writeFile(apiPath, updatedContent, 'utf-8') - console.log(` ${icons.success} ${fmt.success('更新菜单接口完成')}`) - } catch (err) { - console.log(` ${icons.error} ${fmt.error('更新菜单接口失败')}`) - console.log(` ${fmt.dim('错误详情: ' + err)}`) - } -} - -// 用户确认函数 async function getUserConfirmation(): Promise { const { createInterface } = await import('readline') - return new Promise((resolve) => { - const rl = createInterface({ - input: process.stdin, - output: process.stdout - }) - - console.log( - ` ${fmt.highlight('请输入')} ${fmt.success('yes')} ${fmt.highlight('确认执行清理操作,或按 Enter 取消')}` - ) + const rl = createInterface({ input: process.stdin, output: process.stdout }) console.log() - process.stdout.write(` ${icons.arrow} `) - + console.log(` ${fmt.highlight('清空 mock 升级日志')} ${fmt.dim('(src/mock/upgrade/changeLog.ts)')}`) + console.log(` ${fmt.dim('输入 yes 并回车确认,其它键取消')}`) + console.log() + process.stdout.write(' > ') rl.question('', (answer: string) => { rl.close() resolve(answer.toLowerCase().trim() === 'yes') @@ -629,210 +46,51 @@ async function getUserConfirmation(): Promise { }) } -// 显示清理警告 -async function showCleanupWarning() { - createCard('安全警告', [ - `${fmt.warning('此操作将永久删除以下演示内容,且无法恢复!')}`, - `${fmt.dim('请仔细阅读清理列表,确认后再继续操作')}` - ]) +async function resetChangeLog(): Promise { + const changeLogPath = path.resolve(process.cwd(), 'src/mock/upgrade/changeLog.ts') + const cleaned = `import { ref } from 'vue' - const cleanupItems = [ - { - icon: icons.image, - name: '图片资源', - desc: '演示用的封面图片、3D图片、运维图片等', - color: theme.orange - }, - { - icon: icons.file, - name: '演示页面', - desc: 'widgets、template、article、examples、safeguard等页面', - color: theme.purple - }, - { - icon: icons.code, - name: '路由模块文件', - desc: '删除演示路由模块,只保留核心模块(dashboard、system、result、exception)', - color: theme.primary - }, - { - icon: icons.link, - name: '路由别名', - desc: '重写routesAlias.ts,移除演示路由别名', - color: theme.info - }, - { - icon: icons.data, - name: 'Mock数据', - desc: '演示用的JSON数据、文章列表、评论数据等', - color: theme.success - }, - { - icon: icons.globe, - name: '多语言文件', - desc: '清理中英文语言包中的演示菜单项', - color: theme.warning - }, - { icon: icons.map, name: '地图组件', desc: '移除art-map-chart地图组件', color: theme.error }, - { icon: icons.chat, name: '评论组件', desc: '移除comment-widget评论组件', color: theme.orange }, - { - icon: icons.bolt, - name: '快速入口', - desc: '移除分析页、礼花效果、聊天、更新日志、定价、留言管理等无效项目', - color: theme.purple - } - ] - - console.log(` ${fmt.badge('', theme.bgRed)} ${fmt.title('将要清理的内容')}`) - console.log() - - cleanupItems.forEach((item, index) => { - console.log(` ${item.color}${theme.reset} ${fmt.highlight(`${index + 1}. ${item.name}`)}`) - console.log(` ${fmt.dim(item.desc)}`) - }) - - console.log() - console.log(` ${fmt.badge('', theme.bgGreen)} ${fmt.title('保留的功能模块')}`) - console.log() - - const preservedModules = [ - { name: 'Dashboard', desc: '工作台页面' }, - { name: 'System', desc: '系统管理模块' }, - { name: 'Result', desc: '结果页面' }, - { name: 'Exception', desc: '异常页面' }, - { name: 'Auth', desc: '登录注册功能' }, - { name: 'Core Components', desc: '核心组件库' } - ] - - preservedModules.forEach((module) => { - console.log(` ${icons.check} ${fmt.success(module.name)} ${fmt.dim(`- ${module.desc}`)}`) - }) - - console.log() - createDivider() - console.log() +interface UpgradeLog { + version: string + title: string + date: string + detail?: string[] + requireReLogin?: boolean + remark?: string } -// 显示统计信息 -async function showStats() { - const duration = Date.now() - stats.startTime - const seconds = (duration / 1000).toFixed(2) - - console.log() - createCard('清理统计', [ - `${fmt.success('成功删除')}: ${fmt.highlight(stats.deletedFiles.toString())} 个文件`, - `${fmt.info('涉及路径')}: ${fmt.highlight(stats.deletedPaths.toString())} 个目录/文件`, - ...(stats.failedPaths > 0 - ? [ - `${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(stats.failedPaths.toString())} 个路径` - ] - : []), - `${fmt.info('耗时')}: ${fmt.highlight(seconds)} 秒` - ]) +export const upgradeLogList = ref([]) +` + await fs.writeFile(changeLogPath, cleaned, 'utf-8') + console.log(` ${fmt.success('已写入')}: ${changeLogPath}`) } -// 创建成功横幅 -function createSuccessBanner() { - console.log() - console.log( - fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗') - ) - console.log( - fmt.gradient(' ║ ║') - ) - console.log( - ` ║ ${icons.star} ${fmt.success('清理完成!项目已准备就绪')} ${icons.rocket} ║` - ) - console.log( - ` ║ ${fmt.dim('现在可以开始您的开发之旅了!')} ║` - ) - console.log( - fmt.gradient(' ║ ║') - ) - console.log( - fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝') - ) - console.log() -} - -// 主函数 async function main() { - // 清屏并显示横幅 console.clear() - createModernBanner() - - // 显示清理警告 - await showCleanupWarning() - - // 统计文件数量 - console.log(` ${fmt.info('正在统计文件数量...')}`) - stats.totalFiles = await countAllFiles() - - console.log(` ${fmt.info('即将清理')}: ${fmt.highlight(stats.totalFiles.toString())} 个文件`) - console.log(` ${fmt.dim(`涉及 ${targets.length} 个目录/文件路径`)}`) + console.log() + console.log(` ${fmt.title('web-admin · clean-dev(安全模式)')}`) + console.log(` ${fmt.dim('不会删除路由、README、业务页面或重写 routesAlias。')}`) console.log() - // 用户确认 - const confirmed = await getUserConfirmation() - - if (!confirmed) { - console.log(` ${fmt.warning('操作已取消,清理中止')}`) + const ok = await getUserConfirmation() + if (!ok) { + console.log(` ${fmt.warning('已取消')}`) console.log() return } - console.log() - console.log(` ${icons.check} ${fmt.success('确认成功,开始清理...')}`) - console.log() - - // 开始清理过程 - console.log(` ${fmt.badge('步骤 1/6', theme.bgBlue)} ${fmt.title('删除演示文件')}`) - console.log() - for (let i = 0; i < targets.length; i++) { - await remove(targets[i], i) + try { + await resetChangeLog() + console.log() + console.log(` ${fmt.success('完成')}`) + } catch (e) { + console.log(` ${fmt.error('失败: ' + String(e))}`) + process.exit(1) } console.log() - - console.log(` ${fmt.badge('步骤 2/6', theme.bgBlue)} ${fmt.title('清理路由模块')}`) - console.log() - await cleanRouteModules() - console.log() - - console.log(` ${fmt.badge('步骤 3/6', theme.bgBlue)} ${fmt.title('重写路由别名')}`) - console.log() - await cleanRoutesAlias() - console.log() - - console.log(` ${fmt.badge('步骤 4/6', theme.bgBlue)} ${fmt.title('清空变更日志')}`) - console.log() - await cleanChangeLog() - console.log() - - console.log(` ${fmt.badge('步骤 5/6', theme.bgBlue)} ${fmt.title('清理语言文件')}`) - console.log() - await cleanLanguageFiles() - console.log() - - console.log(` ${fmt.badge('步骤 6/7', theme.bgBlue)} ${fmt.title('清理快速入口')}`) - console.log() - await cleanFastEnterComponent() - console.log() - - console.log(` ${fmt.badge('步骤 7/7', theme.bgBlue)} ${fmt.title('更新菜单接口')}`) - console.log() - await updateMenuApi() - - // 显示统计信息 - await showStats() - - // 显示成功横幅 - createSuccessBanner() } main().catch((err) => { - console.log() - console.log(` ${icons.error} ${fmt.error('清理脚本执行出错')}`) - console.log(` ${fmt.dim('错误详情: ' + err)}`) - console.log() + console.error(err) process.exit(1) }) diff --git a/web-admin/src/api/admin.ts b/web-admin/src/api/admin.ts index 21f8647d..ed78b87c 100644 --- a/web-admin/src/api/admin.ts +++ b/web-admin/src/api/admin.ts @@ -1,8 +1,87 @@ import request from '@/utils/http' +import type { AppRouteRecord } from '@/types/router' + +// --------------------------------------------------------------------------- +// 后端菜单 / 角色(与 router 后端模式、用户弹窗对齐) +// --------------------------------------------------------------------------- + +/** 后端菜单模式:拉取动态路由 */ +export async function fetchGetMenuList() { + const res = await request.get({ + url: '/web/menu/list' + }) + if (Array.isArray(res)) return res + return res?.menuList || [] +} + +/** 用户表单等:角色下拉 */ +export function fetchGetRoleList() { + return request.get>({ + url: '/web/admin/roles' + }) +} + +// --------------------------------------------------------------------------- +// 租户(系统管理) +// --------------------------------------------------------------------------- + +export function fetchTenantList(params?: { page?: number; pageSize?: number; keyword?: string }) { + return request.get<{ + list: Api.Tenant.TenantItem[] + total: number + page: number + pageSize: number + totalPages: number + }>({ + url: '/web/admin/tenants', + params + }) +} + +export function fetchCreateTenant(data: { + tenantNo: string + tenantName: string + contactName?: string + contactPhone?: string + orderNum?: number + remark?: string +}) { + return request.post({ + url: '/web/admin/tenants', + data, + showSuccessMessage: true + }) +} + +export function fetchUpdateTenant( + id: string, + data: Partial<{ + tenantNo: string + tenantName: string + contactName: string + contactPhone: string + orderNum: number + remark: string + }> +) { + return request.put({ + url: `/web/admin/tenants/${id}`, + data, + showSuccessMessage: true + }) +} + +export function fetchDeleteTenant(id: string) { + return request.del({ + url: `/web/admin/tenants/${id}`, + showSuccessMessage: true + }) +} + +// --------------------------------------------------------------------------- +// 用户 +// --------------------------------------------------------------------------- -/** - * 获取用户列表 - */ export function fetchUserList(params: { page?: number pageSize?: number @@ -24,18 +103,12 @@ export function fetchUserList(params: { }) } -/** - * 获取用户详情 - */ export function fetchUserDetail(id: string) { return request.get({ url: `/web/admin/users/${id}` }) } -/** - * 创建用户 - */ export function fetchCreateUser(data: { username: string password: string @@ -52,27 +125,24 @@ export function fetchCreateUser(data: { }) } -/** - * 更新用户 - */ -export function fetchUpdateUser(id: string, data: Partial<{ - realName: string - phone?: string - role: string - regionId?: string - regionIds?: string[] - isActive: boolean - avatarUrl?: string | null -}>) { +export function fetchUpdateUser( + id: string, + data: Partial<{ + realName: string + phone?: string + role: string + regionId?: string + regionIds?: string[] + isActive: boolean + avatarUrl?: string | null + }> +) { return request.put({ url: `/web/admin/users/${id}`, params: data }) } -/** - * 可授权菜单 code 列表 - */ export function fetchAssignableMenuCodes() { return request.get<{ list: Array<{ @@ -87,9 +157,6 @@ export function fetchAssignableMenuCodes() { }) } -/** - * 获取用户菜单授权 - */ export function fetchUserMenuScopes(userId: string) { return request.get<{ userId: string @@ -99,9 +166,6 @@ export function fetchUserMenuScopes(userId: string) { }) } -/** - * 更新用户菜单授权(全量覆盖) - */ export function fetchUpdateUserMenuScopes(userId: string, menuCodes: string[]) { return request.post({ url: `/web/admin/users/${userId}/menu-scopes`, @@ -109,41 +173,31 @@ export function fetchUpdateUserMenuScopes(userId: string, menuCodes: string[]) { }) } -/** - * 删除用户 - */ export function fetchDeleteUser(id: string) { return request.del({ url: `/web/admin/users/${id}` }) } -/** - * 重置用户密码 - */ export function fetchResetPassword(id: string) { return request.post({ url: `/web/admin/users/${id}/reset-password` }) } -/** - * 修改用户密码 - */ -export function fetchChangePassword(data: { - oldPassword: string - newPassword: string -}) { - return request.post({ - url: '/web/admin/users/change-password', - params: data +/** 上传用户头像,返回存储后的 URL,供写入 avatarUrl */ +export function fetchUploadUserAvatar(data: FormData) { + return request.post<{ url: string }>({ + url: '/web/admin/users/upload-avatar', + data }) } -/** - * 获取区域列表 - */ -export function fetchRegionList(params?: { +// --------------------------------------------------------------------------- +// 区域(仅列表分页供 fetchNotDeletedRegions 使用 + 启用列表) +// --------------------------------------------------------------------------- + +function fetchRegionList(params?: { page?: number pageSize?: number keyword?: string @@ -161,36 +215,29 @@ export function fetchRegionList(params?: { }) } -/** - * 获取启用的区域列表 - */ export function fetchActiveRegions() { - // 简单缓存:区域列表变化频率低,避免页面间重复请求导致“区域加载慢” - // 5 分钟 TTL;需要实时刷新时可在调用方自行绕过(后续可升级为 Pinia store) const now = Date.now() const anyFn = fetchActiveRegions as any const cache = anyFn.__cache as undefined | { ts: number; data: Api.Region.RegionItem[] } if (cache && now - cache.ts < 5 * 60 * 1000) return Promise.resolve(cache.data) - return request.get({ - url: '/web/admin/regions/active' - }).then((data) => { - anyFn.__cache = { ts: now, data: data || [] } - return data - }) + return request + .get({ + url: '/web/admin/regions/active' + }) + .then((data) => { + anyFn.__cache = { ts: now, data: data || [] } + return data + }) } -/** - * 获取所有未删除的区域(包含禁用态 isActive=0) - * 用于后台各模块“区域 tab”展示全量范围。 - */ +/** 未删除的全量区域(含禁用),供各页区域 Tab */ export async function fetchNotDeletedRegions() { const now = Date.now() const anyFn = fetchNotDeletedRegions as any const cache = anyFn.__cache as undefined | { ts: number; data: Api.Region.RegionItem[] } if (cache && now - cache.ts < 5 * 60 * 1000) return cache.data - // 注意:后端 `GET /web/admin/regions` 的 pageSize 有上限(<=100),不能一次性拉太大。 const pageSize = 100 const all: Api.Region.RegionItem[] = [] let page = 1 @@ -201,7 +248,6 @@ export async function fetchNotDeletedRegions() { all.push(...list) const total = Number(res?.total ?? 0) - // 没有更多数据:直接退出(避免死循环) if (!total || all.length >= total || list.length < pageSize) break page++ } @@ -210,64 +256,10 @@ export async function fetchNotDeletedRegions() { return all } -/** - * 获取区域详情 - */ -export function fetchRegionDetail(id: string) { - return request.get({ - url: `/web/admin/regions/${id}` - }) -} +// --------------------------------------------------------------------------- +// 商品分类 +// --------------------------------------------------------------------------- -/** - * 创建区域 - */ -export function fetchCreateRegion(data: { - name: string - code: string - description?: string - sortOrder?: number - /** 预计配送时长(分钟) */ - expectedDeliveryMinutes?: number | null - /** 计划到货前未送达时预警提前时间(分钟) */ - deliveryWarnAdvanceMinutes?: number | null -}) { - return request.post({ - url: '/web/admin/regions', - params: data - }) -} - -/** - * 更新区域 - */ -export function fetchUpdateRegion(id: string, data: Partial<{ - name: string - code: string - description?: string - sortOrder?: number - isActive?: boolean - expectedDeliveryMinutes?: number | null - deliveryWarnAdvanceMinutes?: number | null -}>) { - return request.put({ - url: `/web/admin/regions/${id}`, - params: data - }) -} - -/** - * 删除区域 - */ -export function fetchDeleteRegion(id: string) { - return request.del({ - url: `/web/admin/regions/${id}` - }) -} - -/** - * 获取商品分类列表 - */ export function fetchCategoryList(params?: { page?: number pageSize?: number @@ -286,24 +278,11 @@ export function fetchCategoryList(params?: { }) } -/** - * 获取商品分类树 - */ -export function fetchCategoryTree() { - return request.get({ - url: '/web/admin/categories/tree' - }) -} - -/** - * 创建商品分类 - */ export function fetchCreateCategory(data: { name: string parentId?: string sortOrder?: number imageUrl?: string - /** 价格系数,百分比点数(如 5 表示 5%) */ priceCoefficient?: number }) { return request.post({ @@ -312,35 +291,29 @@ export function fetchCreateCategory(data: { }) } -/** - * 更新商品分类 - */ -export function fetchUpdateCategory(id: string, data: Partial<{ - name: string - parentId?: string - sortOrder?: number - isActive?: boolean - imageUrl?: string - priceCoefficient?: number -}>) { +export function fetchUpdateCategory( + id: string, + data: Partial<{ + name: string + parentId?: string + sortOrder?: number + isActive?: boolean + imageUrl?: string + priceCoefficient?: number + }> +) { return request.put({ url: `/web/admin/categories/${id}`, params: data }) } -/** - * 删除商品分类 - */ export function fetchDeleteCategory(id: string) { return request.del({ url: `/web/admin/categories/${id}` }) } -/** - * 上传商品分类图片(MinIO)并返回图片URL - */ export function fetchUploadCategoryImage(data: FormData) { return request.post({ url: '/web/admin/categories/upload-image', @@ -348,17 +321,10 @@ export function fetchUploadCategoryImage(data: FormData) { }) } -/** 上传用户头像,返回 MinIO 等存储后的 URL,供写入 avatarUrl */ -export function fetchUploadUserAvatar(data: FormData) { - return request.post<{ url: string }>({ - url: '/web/admin/users/upload-avatar', - data - }) -} +// --------------------------------------------------------------------------- +// 商品单位 +// --------------------------------------------------------------------------- -/** - * 获取商品单位列表 - */ export function fetchUnitList(params?: { page?: number pageSize?: number @@ -377,45 +343,37 @@ export function fetchUnitList(params?: { }) } -/** - * 创建商品单位 - */ -export function fetchCreateUnit(data: { - name: string - code: string -}) { +export function fetchCreateUnit(data: { name: string; code: string }) { return request.post({ url: '/web/admin/units', params: data }) } -/** - * 更新商品单位 - */ -export function fetchUpdateUnit(id: string, data: Partial<{ - name: string - code: string - isActive?: boolean -}>) { +export function fetchUpdateUnit( + id: string, + data: Partial<{ + name: string + code: string + isActive?: boolean + }> +) { return request.put({ url: `/web/admin/units/${id}`, params: data }) } -/** - * 删除商品单位 - */ export function fetchDeleteUnit(id: string) { return request.del({ url: `/web/admin/units/${id}` }) } -/** - * 获取商品列表 - */ +// --------------------------------------------------------------------------- +// 商品 +// --------------------------------------------------------------------------- + export function fetchProductList(params?: { page?: number pageSize?: number @@ -435,18 +393,12 @@ export function fetchProductList(params?: { }) } -/** - * 获取商品详情 - */ export function fetchProductDetail(id: string) { return request.get({ url: `/web/admin/products/${id}` }) } -/** - * 商品在各启用区域的当前生效价(来自该区域生效中的价格清单) - */ export function fetchProductRegionPrices(productId: string) { return request.get<{ list: Array<{ @@ -461,9 +413,6 @@ export function fetchProductRegionPrices(productId: string) { }) } -/** - * 创建商品 - */ export function fetchCreateProduct(data: { name: string skuCode?: string @@ -472,7 +421,6 @@ export function fetchCreateProduct(data: { measureValue?: number | null description?: string imageUrl?: string - /** 调价系数(%):百分比点数,5 表示 5% */ priceCoefficient?: number isActive?: boolean detailContent?: string @@ -483,41 +431,35 @@ export function fetchCreateProduct(data: { }) } -/** - * 更新商品 - */ -export function fetchUpdateProduct(id: string, data: Partial<{ - name: string - skuCode?: string - categoryId?: string - unitId?: string - measureValue?: number | null - description?: string - imageUrl?: string - priceCoefficient?: number - isActive?: boolean - detailContent?: string -}>) { +export function fetchUpdateProduct( + id: string, + data: Partial<{ + name: string + skuCode?: string + categoryId?: string + unitId?: string + measureValue?: number | null + description?: string + imageUrl?: string + priceCoefficient?: number + isActive?: boolean + detailContent?: string + }> +) { return request.put({ url: `/web/admin/products/${id}`, params: data }) } -/** - * 删除商品 - */ export function fetchDeleteProduct(id: string) { return request.del({ url: `/web/admin/products/${id}` }) } -// ========== 商品轮播图 ========== +// --- 轮播图 --- -/** - * 获取商品轮播图列表 - */ export function fetchProductBannerList(productId: string) { return request.get<{ list: Api.Product.ProductBanner[] @@ -527,9 +469,6 @@ export function fetchProductBannerList(productId: string) { }) } -/** - * 批量添加商品轮播图 - */ export function fetchCreateProductBanners(productId: string, items: Api.Product.ProductBannerCreate[]) { return request.post({ url: `/web/admin/product-banners/product/${productId}`, @@ -537,19 +476,6 @@ export function fetchCreateProductBanners(productId: string, items: Api.Product. }) } -/** - * 更新轮播图 - */ -export function fetchUpdateProductBanner(id: string, data: Partial) { - return request.put({ - url: `/web/admin/product-banners/${id}`, - params: data - }) -} - -/** - * 更新轮播图排序 - */ export function fetchUpdateProductBannerSort(productId: string, items: { id: string; sortOrder: number }[]) { return request.put({ url: `/web/admin/product-banners/product/${productId}/sort`, @@ -557,20 +483,14 @@ export function fetchUpdateProductBannerSort(productId: string, items: { id: str }) } -/** - * 删除轮播图 - */ export function fetchDeleteProductBanner(id: string) { return request.del({ url: `/web/admin/product-banners/${id}` }) } -// ========== 商品详情媒体 ========== +// --- 详情媒体 --- -/** - * 获取商品详情媒体列表 - */ export function fetchProductDetailMediaList(productId: string) { return request.get<{ list: Api.Product.ProductDetailMedia[] @@ -580,29 +500,16 @@ export function fetchProductDetailMediaList(productId: string) { }) } -/** - * 批量添加商品详情媒体 - */ -export function fetchCreateProductDetailMedia(productId: string, items: Api.Product.ProductDetailMediaCreate[]) { +export function fetchCreateProductDetailMedia( + productId: string, + items: Api.Product.ProductDetailMediaCreate[] +) { return request.post({ url: `/web/admin/product-detail-media/product/${productId}`, params: items }) } -/** - * 更新详情媒体 - */ -export function fetchUpdateProductDetailMedia(id: string, data: Partial) { - return request.put({ - url: `/web/admin/product-detail-media/${id}`, - params: data - }) -} - -/** - * 更新详情媒体排序 - */ export function fetchUpdateProductDetailMediaSort(productId: string, items: { id: string; sortOrder: number }[]) { return request.put({ url: `/web/admin/product-detail-media/product/${productId}/sort`, @@ -610,40 +517,14 @@ export function fetchUpdateProductDetailMediaSort(productId: string, items: { id }) } -/** - * 删除详情媒体 - */ export function fetchDeleteProductDetailMedia(id: string) { return request.del({ url: `/web/admin/product-detail-media/${id}` }) } -// ========== 商品参数模板 ========== +// --- 参数模板(简洁列表供分类/商品选择) --- -/** - * 获取参数模板列表 - */ -export function fetchProductParameterList(params?: { - page?: number - pageSize?: number - keyword?: string - isActive?: number -}) { - return request.get<{ - list: Api.Product.ProductParameter[] - total: number - page: number - pageSize: number - }>({ - url: '/web/admin/product-parameters', - params - }) -} - -/** - * 获取参数模板简洁列表 - */ export function fetchProductParameterSimple() { return request.get<{ list: Api.Product.ProductParameterSimple[] @@ -653,49 +534,8 @@ export function fetchProductParameterSimple() { }) } -/** - * 获取参数模板详情 - */ -export function fetchProductParameterDetail(id: string) { - return request.get({ - url: `/web/admin/product-parameters/${id}` - }) -} +// --- 分类关联参数 --- -/** - * 创建参数模板 - */ -export function fetchCreateProductParameter(data: Api.Product.ProductParameterCreate) { - return request.post({ - url: '/web/admin/product-parameters', - params: data - }) -} - -/** - * 更新参数模板 - */ -export function fetchUpdateProductParameter(id: string, data: Partial) { - return request.put({ - url: `/web/admin/product-parameters/${id}`, - params: data - }) -} - -/** - * 删除参数模板 - */ -export function fetchDeleteProductParameter(id: string) { - return request.del({ - url: `/web/admin/product-parameters/${id}` - }) -} - -// ========== 分类参数关联 ========== - -/** - * 获取分类关联的参数列表 - */ export function fetchCategoryParameters(categoryId: string) { return request.get<{ list: Api.Product.CategoryParameter[] @@ -705,9 +545,6 @@ export function fetchCategoryParameters(categoryId: string) { }) } -/** - * 更新分类关联的参数 - */ export function fetchUpdateCategoryParameters(categoryId: string, items: Api.Product.CategoryParameterCreate[]) { return request.put({ url: `/web/admin/categories/${categoryId}/parameters`, @@ -715,11 +552,8 @@ export function fetchUpdateCategoryParameters(categoryId: string, items: Api.Pro }) } -// ========== 商品参数值 ========== +// --- 商品参数值 --- -/** - * 获取商品参数值列表 - */ export function fetchProductParameterValues(productId: string) { return request.get<{ list: Api.Product.ProductParameterValue[] @@ -729,97 +563,12 @@ export function fetchProductParameterValues(productId: string) { }) } -/** - * 批量保存商品参数值 - */ -export function fetchSaveProductParameterValues(productId: string, values: Api.Product.ProductParameterValueCreate[]) { +export function fetchSaveProductParameterValues( + productId: string, + values: Api.Product.ProductParameterValueCreate[] +) { return request.post({ url: `/web/admin/product-parameter-values/product/${productId}`, params: { productId, values } }) } - -// ========== 售后评价 ========== - -/** - * 获取评价列表 - */ -export function fetchReviewList(params?: { - page?: number - pageSize?: number - productId?: string - productName?: string - userName?: string - status?: string - rating?: number - startDate?: string - endDate?: string -}) { - return request.get<{ - list: Api.Product.ProductReview[] - total: number - page: number - pageSize: number - }>({ - url: '/web/admin/reviews', - params - }) -} - -/** - * 获取评价统计 - */ -export function fetchReviewStatistics(productId?: string) { - return request.get({ - url: '/web/admin/reviews/statistics', - params: { productId } - }) -} - -/** - * 获取评价详情 - */ -export function fetchReviewDetail(id: string) { - return request.get({ - url: `/web/admin/reviews/${id}` - }) -} - -/** - * 审核通过评价 - */ -export function fetchApproveReview(id: string) { - return request.put({ - url: `/web/admin/reviews/${id}/approve` - }) -} - -/** - * 拒绝评价 - */ -export function fetchRejectReview(id: string, rejectReason: string) { - return request.put({ - url: `/web/admin/reviews/${id}/reject`, - params: { rejectReason } - }) -} - -/** - * 回复评价 - */ -export function fetchReplyReview(id: string, replyContent: string) { - return request.put({ - url: `/web/admin/reviews/${id}/reply`, - params: { replyContent } - }) -} - -/** - * 置顶/取消置顶评价 - */ -export function fetchToggleReviewTop(id: string, isTop: number) { - return request.put({ - url: `/web/admin/reviews/${id}/top`, - params: { isTop } - }) -} diff --git a/web-admin/src/api/business.ts b/web-admin/src/api/business.ts new file mode 100644 index 00000000..9f20b2a6 --- /dev/null +++ b/web-admin/src/api/business.ts @@ -0,0 +1,146 @@ +import request from '@/utils/http' + +/** + * 订单与订单页依赖的少量接口(其余业务 API 已移除,需要时再加回)。 + */ + +export function fetchOrderList(params: { + page?: number + pageSize?: number + keyword?: string + status?: string + orderStatus?: string + deliveryStatus?: string + payStatus?: string + paymentMethod?: string + regionId?: string + clientId?: string + startDate?: string + endDate?: string +}) { + return request.get<{ + list: Api.Order.OrderItem[] + total: number + page: number + pageSize: number + totalPages: number + }>({ + url: '/web/admin/orders', + params + }) +} + +export function fetchOrderDetail(id: string) { + return request.get({ + url: `/web/admin/orders/${id}` + }) +} + +export function fetchAssignDelivery(data: { + orderId: string + deliverId: string + deliveryDeadlineAt: string + dispatchRemark?: string +}) { + return request.post({ + url: `/web/admin/orders/${data.orderId}/assign-deliverer`, + params: { + deliverId: data.deliverId, + deliveryDeadlineAt: data.deliveryDeadlineAt, + dispatchRemark: data.dispatchRemark || '' + } + }) +} + +export function fetchReviewCancel(data: { orderId: string; approved: boolean; note?: string }) { + return request.post({ + url: `/web/admin/orders/${data.orderId}/review-cancel`, + params: { approved: data.approved, note: data.note } + }) +} + +export function fetchCancelOrder(data: { + orderId: string + reason: string + note?: string + evidenceImageUrls?: string[] +}) { + return request.post({ + url: `/web/admin/orders/${data.orderId}/cancel`, + params: { + reason: data.reason, + note: data.note, + evidenceImageUrls: data.evidenceImageUrls || [] + } + }) +} + +export function fetchOrderAdjusts(orderId: string) { + return request.get({ + url: `/web/admin/orders/${orderId}/adjusts` + }) +} + +export function fetchAdjustOrder(data: { + orderId: string + adjustments?: Array<{ + orderItemId: string + adjustType: 'reduce' | 'delete' | 'substitute' | 'increase' + newQuantity?: number + newProductId?: string + reason: string + }> + addressUpdate?: { + deliveryAddress?: string + contactPerson?: string + contactPhone?: string + reason?: string + } +}) { + return request.post({ + url: `/web/admin/orders/${data.orderId}/adjust`, + params: { + adjustments: data.adjustments || [], + addressUpdate: data.addressUpdate || undefined + } + }) +} + +export function fetchOrderItems(orderId: string) { + return request.get({ + url: `/web/admin/orders/${orderId}/items` + }) +} + +export function fetchPriceVersionDetail(id: string) { + return request.get({ + url: `/web/admin/price-versions/${id}` + }) +} + +export function fetchUploadAttachment(data: FormData) { + return request.post<{ + id: string + fileName: string + fileUrl: string + fileSize: number + fileSizeDisplay?: string + fileType?: string + }>({ + url: '/web/admin/attachment/upload', + data + }) +} + +export function fetchUploadProductImage(data: FormData) { + return request.post({ + url: '/web/admin/products/upload-image', + data + }) +} + +export function fetchSystemConfig() { + return request.get({ + url: '/web/admin/system-config/settings' + }) +} diff --git a/web-admin/src/api/system-manage.ts b/web-admin/src/api/system-manage.ts deleted file mode 100644 index 8de96912..00000000 --- a/web-admin/src/api/system-manage.ts +++ /dev/null @@ -1,39 +0,0 @@ -import request from '@/utils/http' -import type { AppRouteRecord } from '@/types/router' - -/** - * 获取菜单列表 - * 从后端获取运营端菜单 - */ -export async function fetchGetMenuList() { - const res = await request.get({ - url: '/web/menu/list' - }) - if (Array.isArray(res)) return res - return res?.menuList || [] -} - -/** - * 获取角色列表 - */ -export function fetchGetRoleList() { - return request.get>({ - url: '/web/admin/roles' - }) -} - -/** - * 示例表格用户列表(兼容旧示例页调用) - */ -export function fetchGetUserList(params?: Record) { - return request.get<{ - list: any[] - total: number - page?: number - pageSize?: number - }>({ - url: '/web/admin/users', - params - }) -} - diff --git a/web-admin/src/components/business/order-operate-dialog-bridge/index.vue b/web-admin/src/components/business/order-operate-dialog-bridge/index.vue new file mode 100644 index 00000000..f7de4267 --- /dev/null +++ b/web-admin/src/components/business/order-operate-dialog-bridge/index.vue @@ -0,0 +1,129 @@ + - - diff --git a/web-admin/src/views/style-reference/delivery-demo/index.vue b/web-admin/src/views/style-reference/delivery-demo/index.vue deleted file mode 100644 index ca4de228..00000000 --- a/web-admin/src/views/style-reference/delivery-demo/index.vue +++ /dev/null @@ -1,1088 +0,0 @@ - - - - - diff --git a/web-admin/src/views/style-reference/login-sample/index.vue b/web-admin/src/views/style-reference/login-sample/index.vue deleted file mode 100644 index c3e06f47..00000000 --- a/web-admin/src/views/style-reference/login-sample/index.vue +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - diff --git a/web-admin/src/views/style-reference/mint-tokens/index.vue b/web-admin/src/views/style-reference/mint-tokens/index.vue deleted file mode 100644 index faa4a166..00000000 --- a/web-admin/src/views/style-reference/mint-tokens/index.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - diff --git a/web-admin/src/views/system/category/index.vue b/web-admin/src/views/system/category/index.vue new file mode 100644 index 00000000..55f6ec1a --- /dev/null +++ b/web-admin/src/views/system/category/index.vue @@ -0,0 +1,1569 @@ + + + + + + + diff --git a/web-admin/src/views/system/order/index.vue b/web-admin/src/views/system/order/index.vue new file mode 100644 index 00000000..35151b37 --- /dev/null +++ b/web-admin/src/views/system/order/index.vue @@ -0,0 +1,3811 @@ + + + + + + + + + + + diff --git a/web-admin/src/views/system/product/index.vue b/web-admin/src/views/system/product/index.vue new file mode 100644 index 00000000..8a73c61f --- /dev/null +++ b/web-admin/src/views/system/product/index.vue @@ -0,0 +1,3009 @@ + + + + + + + diff --git a/web-admin/src/views/system/tenant/index.vue b/web-admin/src/views/system/tenant/index.vue new file mode 100644 index 00000000..9ad0e3db --- /dev/null +++ b/web-admin/src/views/system/tenant/index.vue @@ -0,0 +1,327 @@ + + + + + + + diff --git a/web-admin/src/views/system/unit/index.vue b/web-admin/src/views/system/unit/index.vue new file mode 100644 index 00000000..0f852464 --- /dev/null +++ b/web-admin/src/views/system/unit/index.vue @@ -0,0 +1,434 @@ + + + + + diff --git a/web-admin/src/views/system/user/detail.vue b/web-admin/src/views/system/user/detail.vue new file mode 100644 index 00000000..595a2e9c --- /dev/null +++ b/web-admin/src/views/system/user/detail.vue @@ -0,0 +1,797 @@ + + + + + + diff --git a/web-admin/src/views/system/user/index.vue b/web-admin/src/views/system/user/index.vue new file mode 100644 index 00000000..673e83c0 --- /dev/null +++ b/web-admin/src/views/system/user/index.vue @@ -0,0 +1,1575 @@ + + + + + + + diff --git a/web-admin/src/views/system/user/modules/user-dialog.vue b/web-admin/src/views/system/user/modules/user-dialog.vue new file mode 100644 index 00000000..7675fa95 --- /dev/null +++ b/web-admin/src/views/system/user/modules/user-dialog.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/web-admin/src/views/system/user/modules/user-search.vue b/web-admin/src/views/system/user/modules/user-search.vue new file mode 100644 index 00000000..7282a8e6 --- /dev/null +++ b/web-admin/src/views/system/user/modules/user-search.vue @@ -0,0 +1,112 @@ + + +