56 KiB
渠道服务购买订单 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 实现渠道服务购买订单功能,包括充值/购买两种订单类型、渠道账户余额管理、产品份数库存管理、财务确认流程。
Architecture: 扩展现有 biz_channel_service_order 表和 pilot_service.py 模块,新增 2 张表(渠道账户 + 产品份数库存)。后端遵循现有 _row_to_dict + CRUD + API 三层模式,前端新增 2 个页面 + 1 个渠道页扩展。
Tech Stack: FastAPI + SQLAlchemy + Pydantic v2 (后端) · Vue 3 + Element Plus + Tailwind CSS 4 (前端) · MySQL 5.7
Design Spec: docs/superpowers/specs/2026-05-24-channel-service-order-design.md
File Map
| Action | File | Responsibility |
|---|---|---|
| CREATE | docs/sql/channel-service-order-migration.sql |
数据库迁移脚本 |
| MODIFY | backend/app/models/models.py |
新增 BizChannelAccount, BizChannelProductQuota 模型 |
| MODIFY | backend/app/crud/pilot_service.py |
新增订单 CRUD + 状态流转 + 账户/份数辅助函数 |
| MODIFY | backend/app/api/pilot_service.py |
新增订单 API 端点 + Pydantic Body |
| MODIFY | web-admin/src/api/admin.ts |
新增前端 API 函数 |
| MODIFY | web-admin/src/utils/status-tag-registry.ts |
新增状态标签 |
| CREATE | web-admin/src/views/pilot-service/channel-service-order/index.vue |
订单管理页 |
| CREATE | web-admin/src/views/pilot-service/channel-service-order-finance/index.vue |
财务确认页 |
| MODIFY | web-admin/src/views/pilot-service/channel/ChannelViewDialog.vue |
账户信息 Tab |
Task 1: 数据库迁移脚本
Files:
-
Create:
docs/sql/channel-service-order-migration.sql -
Step 1: 编写迁移 SQL
-- =============================================
-- 渠道服务购买订单 - 数据库迁移
-- 日期: 2026-05-24
-- =============================================
-- 1. 扩展 biz_channel_service_order 表
ALTER TABLE biz_channel_service_order
ADD COLUMN channel_agreement_id VARCHAR(64) DEFAULT NULL COMMENT '渠道协议ID(购买订单必填)',
ADD COLUMN balance_before DECIMAL(18,2) DEFAULT NULL COMMENT '操作前余额快照',
ADD COLUMN balance_after DECIMAL(18,2) DEFAULT NULL COMMENT '操作后余额快照',
ADD COLUMN finance_confirm_time DATETIME DEFAULT NULL COMMENT '财务确认时间',
ADD COLUMN finance_confirm_attachments VARCHAR(4000) DEFAULT NULL COMMENT '财务确认附件(逗号分隔URL)',
ADD COLUMN finance_remark VARCHAR(512) DEFAULT NULL COMMENT '财务确认备注',
ADD COLUMN refund_apply_by VARCHAR(64) DEFAULT NULL COMMENT '退费申请人ID',
ADD COLUMN refund_apply_time DATETIME DEFAULT NULL COMMENT '退费申请时间',
ADD COLUMN refund_amount DECIMAL(18,2) DEFAULT NULL COMMENT '退费金额',
ADD COLUMN refund_remark VARCHAR(512) DEFAULT NULL COMMENT '退费备注',
ADD COLUMN refund_attachments VARCHAR(4000) DEFAULT NULL COMMENT '退费附件(逗号分隔URL)',
ADD COLUMN refund_finance_confirm_time DATETIME DEFAULT NULL COMMENT '退费财务确认时间',
ADD COLUMN refund_finance_confirm_attachments VARCHAR(4000) DEFAULT NULL COMMENT '退费财务确认附件(逗号分隔URL)',
ADD COLUMN refund_finance_remark VARCHAR(512) DEFAULT NULL COMMENT '退费财务确认备注';
-- 2. 新增 渠道账户余额 表
CREATE TABLE IF NOT EXISTS biz_channel_account (
id VARCHAR(64) NOT NULL COMMENT '主键ID',
channel_id VARCHAR(64) NOT NULL COMMENT '渠道ID',
balance_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 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(4) NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_account_channel (channel_id, del_flag),
KEY idx_ca_tenant (tenant_no, del_flag)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道账户余额';
-- 3. 新增 渠道产品份数库存 表
CREATE TABLE IF NOT EXISTS biz_channel_product_quota (
id VARCHAR(64) NOT NULL COMMENT '主键ID',
channel_id VARCHAR(64) NOT NULL COMMENT '渠道ID',
project_code VARCHAR(64) DEFAULT NULL COMMENT '项目编码',
project_name VARCHAR(200) 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 '规格',
total_qty DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '总购买份数',
used_qty DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '已使用份数(中试服务扣减)',
remaining_qty DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '剩余份数 = total_qty - used_qty',
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(4) NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_quota_channel_product (channel_id, project_code, cell_type_code, spec_text, del_flag),
KEY idx_cpq_tenant (tenant_no, del_flag)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道产品份数库存';
-- 4. 菜单数据(在中试服务管理 parent_id 下新增)
-- 注意: parent_id 需要替换为实际的「中试服务管理」菜单 ID
-- 先查询: SELECT id FROM sys_menu WHERE menu_name='中试服务管理' AND parent_id IN (SELECT id FROM sys_menu WHERE menu_name='web端菜单');
-- 以下用占位 PARENT_ID 表示,执行时替换
SET @pilot_parent_id = (SELECT id FROM sys_menu WHERE menu_name='中试服务管理' AND parent_id IN (SELECT id FROM sys_menu WHERE menu_name='web端菜单') LIMIT 1);
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_time, update_time)
VALUES (REPLACE(UUID(),'-',''), '渠道服务购买订单', @pilot_parent_id, 60, 'channel-service-order', 'pilot-service/channel-service-order/index', 1, '1', '1', '', '', NOW(), NOW());
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_time, update_time)
VALUES (REPLACE(UUID(),'-',''), '渠道服务购买订单(财务确认)', @pilot_parent_id, 61, 'channel-service-order-finance', 'pilot-service/channel-service-order-finance/index', 1, '1', '1', '', '', NOW(), NOW());
- Step 2: 在目标数据库执行迁移
Run: 在 MySQL 客户端中执行此 SQL 文件。
- Step 3: Commit
git add docs/sql/channel-service-order-migration.sql
git commit -m "feat(channel-order): add database migration for channel service order"
Task 2: 后端 SQLAlchemy 模型
Files:
-
Modify:
backend/app/models/models.py(在BizChannelMailAddress类之后,约 L1714 后追加) -
Step 1: 新增 BizChannelAccount 和 BizChannelProductQuota 模型
在 BizChannelMailAddress 类之后、BizCheckupInstitution 类之前追加以下代码:
class BizChannelAccount(Base):
"""渠道账户余额 biz_channel_account"""
__tablename__ = "biz_channel_account"
id = Column(String(64), primary_key=True, comment="主键ID")
channel_id = Column(String(64), nullable=False, comment="渠道ID")
balance_amount = Column(Numeric(18, 2), nullable=False, default=0, 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 BizChannelProductQuota(Base):
"""渠道产品份数库存 biz_channel_product_quota"""
__tablename__ = "biz_channel_product_quota"
id = Column(String(64), primary_key=True, comment="主键ID")
channel_id = Column(String(64), nullable=False, comment="渠道ID")
project_code = Column(String(64), nullable=True, comment="项目编码")
project_name = Column(String(200), nullable=True, comment="项目名称")
cell_type_code = Column(String(64), nullable=True, comment="细胞类型编码")
cell_type_name = Column(String(200), nullable=True, comment="细胞类型名称")
spec_text = Column(String(200), nullable=True, comment="规格")
total_qty = Column(Numeric(18, 4), nullable=False, default=0, comment="总购买份数")
used_qty = Column(Numeric(18, 4), nullable=False, default=0, comment="已使用份数")
remaining_qty = Column(Numeric(18, 4), nullable=False, default=0, 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="删除标记")
- Step 2: 确认 BizChannelServiceOrder 和 BizChannelServiceOrderProduct 模型
检查 models.py 中是否已存在 BizChannelServiceOrder 和 BizChannelServiceOrderProduct 模型。如果存在,需要添加新字段(channel_agreement_id, balance_before, balance_after, finance_confirm_time 等)。如果不存在,需要新建。参照 BizChannelAgreement 的字段模式。
新增字段追加到 BizChannelServiceOrder 模型中(在 order_status 字段之后、tenant_no 之前):
channel_agreement_id = Column(String(64), nullable=True, comment="渠道协议ID(购买订单必填)")
balance_before = Column(Numeric(18, 2), nullable=True, comment="操作前余额快照")
balance_after = Column(Numeric(18, 2), nullable=True, comment="操作后余额快照")
finance_confirm_time = Column(DateTime, nullable=True, comment="财务确认时间")
finance_confirm_attachments = Column(String(4000), nullable=True, comment="财务确认附件(逗号分隔URL)")
finance_remark = Column(String(512), nullable=True, comment="财务确认备注")
refund_apply_by = Column(String(64), nullable=True, comment="退费申请人ID")
refund_apply_time = Column(DateTime, nullable=True, comment="退费申请时间")
refund_amount = Column(Numeric(18, 2), nullable=True, comment="退费金额")
refund_remark = Column(String(512), nullable=True, comment="退费备注")
refund_attachments = Column(String(4000), nullable=True, comment="退费附件(逗号分隔URL)")
refund_finance_confirm_time = Column(DateTime, nullable=True, comment="退费财务确认时间")
refund_finance_confirm_attachments = Column(String(4000), nullable=True, comment="退费财务确认附件")
refund_finance_remark = Column(String(512), nullable=True, comment="退费财务确认备注")
- Step 3: 验证后端启动
Run: cd backend && source venv/bin/activate && python -c "from app.models.models import BizChannelAccount, BizChannelProductQuota, BizChannelServiceOrder, BizChannelServiceOrderProduct; print('Models OK')"
- Step 4: Commit
git add backend/app/models/models.py
git commit -m "feat(channel-order): add SQLAlchemy models for channel account, product quota, and service order fields"
Task 3: 后端 CRUD — 账户 & 份数辅助函数
Files:
- Modify:
backend/app/crud/pilot_service.py(文件末尾追加)
先实现底层辅助函数,因为后续订单 CRUD 依赖它们。
- Step 1: 在
pilot_service.py的 import 区添加新模型引用
在文件头部的 from app.models.models import ... 中追加:BizChannelAccount, BizChannelProductQuota, BizChannelServiceOrder, BizChannelServiceOrderProduct。
- Step 2: 在文件末尾追加账户辅助函数
# ────────────────────────────────────────────
# Channel Account 渠道账户
# ────────────────────────────────────────────
def _get_or_create_channel_account(db, channel_id, tenant_no="zkxw", user_id=None):
"""获取渠道账户,不存在则创建(余额为 0)"""
account = (
db.query(BizChannelAccount)
.filter(
and_(
BizChannelAccount.channel_id == channel_id,
BizChannelAccount.del_flag == 0,
)
)
.with_for_update()
.first()
)
if not account:
account = BizChannelAccount(
id=generate_uuid(),
channel_id=channel_id,
balance_amount=0,
tenant_no=tenant_no,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(account)
db.flush()
return account
def get_channel_account(db, channel_id, tenant_no=None):
"""获取渠道账户(只读,不加锁)"""
q = db.query(BizChannelAccount).filter(
and_(BizChannelAccount.channel_id == channel_id, BizChannelAccount.del_flag == 0)
)
if tenant_no:
q = q.filter(BizChannelAccount.tenant_no == tenant_no)
return q.first()
def _row_to_dict_channel_account(row) -> Dict[str, Any]:
return {
"id": row.id,
"channelId": row.channel_id or "",
"balanceAmount": float(row.balance_amount) if row.balance_amount else 0.0,
"tenantNo": row.tenant_no or "",
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
}
- Step 3: 追加份数库存辅助函数
# ────────────────────────────────────────────
# Channel Product Quota 渠道产品份数库存
# ────────────────────────────────────────────
def _upsert_channel_product_quota(db, channel_id, project_code, project_name,
cell_type_code, cell_type_name, spec_text,
add_qty, tenant_no="zkxw", user_id=None):
"""新增或更新渠道产品份数(购买时调用,add_qty > 0)"""
quota = (
db.query(BizChannelProductQuota)
.filter(
and_(
BizChannelProductQuota.channel_id == channel_id,
BizChannelProductQuota.project_code == project_code,
BizChannelProductQuota.cell_type_code == cell_type_code,
BizChannelProductQuota.spec_text == spec_text,
BizChannelProductQuota.del_flag == 0,
)
)
.first()
)
if quota:
quota.total_qty = (quota.total_qty or 0) + add_qty
quota.remaining_qty = (quota.remaining_qty or 0) + add_qty
quota.update_by = user_id
quota.update_time = datetime.now()
db.add(quota)
else:
quota = BizChannelProductQuota(
id=generate_uuid(),
channel_id=channel_id,
project_code=project_code,
project_name=project_name,
cell_type_code=cell_type_code,
cell_type_name=cell_type_name,
spec_text=spec_text,
total_qty=add_qty,
used_qty=0,
remaining_qty=add_qty,
tenant_no=tenant_no,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(quota)
db.flush()
return quota
def _reduce_channel_product_quota(db, channel_id, project_code, cell_type_code,
spec_text, reduce_qty, user_id=None):
"""扣减渠道产品份数(作废时调用,reduce_qty > 0)"""
quota = (
db.query(BizChannelProductQuota)
.filter(
and_(
BizChannelProductQuota.channel_id == channel_id,
BizChannelProductQuota.project_code == project_code,
BizChannelProductQuota.cell_type_code == cell_type_code,
BizChannelProductQuota.spec_text == spec_text,
BizChannelProductQuota.del_flag == 0,
)
)
.first()
)
if not quota:
raise ValueError(f"渠道产品份数不存在: {project_code}/{cell_type_code}/{spec_text}")
if (quota.remaining_qty or 0) < reduce_qty:
raise ValueError(
f"产品 {quota.project_name}({quota.spec_text}) 已使用份数,无法完全作废。"
f"剩余={quota.remaining_qty}, 需退回={reduce_qty}"
)
quota.total_qty = (quota.total_qty or 0) - reduce_qty
quota.remaining_qty = (quota.remaining_qty or 0) - reduce_qty
quota.update_by = user_id
quota.update_time = datetime.now()
db.add(quota)
db.flush()
return quota
def list_channel_product_quotas(db, channel_id, tenant_no=None):
"""获取渠道产品份数列表"""
q = db.query(BizChannelProductQuota).filter(
and_(
BizChannelProductQuota.channel_id == channel_id,
BizChannelProductQuota.del_flag == 0,
)
)
if tenant_no:
q = q.filter(BizChannelProductQuota.tenant_no == tenant_no)
rows = q.order_by(BizChannelProductQuota.create_time.desc()).all()
return [_row_to_dict_channel_product_quota(x) for x in rows]
def _row_to_dict_channel_product_quota(row) -> Dict[str, Any]:
return {
"id": row.id,
"channelId": row.channel_id or "",
"projectCode": row.project_code or "",
"projectName": row.project_name or "",
"cellTypeCode": row.cell_type_code or "",
"cellTypeName": row.cell_type_name or "",
"specText": row.spec_text or "",
"totalQty": float(row.total_qty) if row.total_qty else 0,
"usedQty": float(row.used_qty) if row.used_qty else 0,
"remainingQty": float(row.remaining_qty) if row.remaining_qty else 0,
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
}
- Step 4: Commit
git add backend/app/crud/pilot_service.py
git commit -m "feat(channel-order): add CRUD helpers for channel account and product quota"
Task 4: 后端 CRUD — 订单基础操作 (list/get/create/update/delete)
Files:
-
Modify:
backend/app/crud/pilot_service.py(继续在末尾追加) -
Step 1: 追加 _row_to_dict 和 list/get 函数
# ────────────────────────────────────────────
# Channel Service Order 渠道服务购买订单
# ────────────────────────────────────────────
def _row_to_dict_service_order(row, db=None, tenant_no=None) -> Dict[str, Any]:
tn = tenant_no or row.tenant_no
from app.utils.minio import urls_to_attachment_list
result = {
"id": row.id,
"channelId": row.channel_id or "",
"channelName": row.channel_name or "",
"deptName": row.dept_name or "",
"orderType": row.order_type or "",
"prepayAmount": float(row.prepay_amount) if row.prepay_amount else None,
"purchaseTotalAmount": float(row.purchase_total_amount) if row.purchase_total_amount else None,
"orderStatus": row.order_status or "",
"channelAgreementId": row.channel_agreement_id or "",
"balanceBefore": float(row.balance_before) if row.balance_before else None,
"balanceAfter": float(row.balance_after) if row.balance_after else None,
"financeConfirmTime": row.finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_confirm_time else None,
"financeConfirmAttachments": row.finance_confirm_attachments or "",
"financeConfirmAttachmentFiles": urls_to_attachment_list(row.finance_confirm_attachments, db, tn) if db and row.finance_confirm_attachments else [],
"financeRemark": row.finance_remark or "",
"refundApplyBy": row.refund_apply_by or "",
"refundApplyTime": row.refund_apply_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_apply_time else None,
"refundAmount": float(row.refund_amount) if row.refund_amount else None,
"refundRemark": row.refund_remark or "",
"refundAttachments": row.refund_attachments or "",
"refundAttachmentFiles": urls_to_attachment_list(row.refund_attachments, db, tn) if db and row.refund_attachments else [],
"refundFinanceConfirmTime": row.refund_finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_finance_confirm_time else None,
"refundFinanceConfirmAttachments": row.refund_finance_confirm_attachments or "",
"refundFinanceConfirmAttachmentFiles": urls_to_attachment_list(row.refund_finance_confirm_attachments, db, tn) if db and row.refund_finance_confirm_attachments else [],
"refundFinanceRemark": row.refund_finance_remark or "",
"tenantNo": row.tenant_no or "",
"remark": row.remark or "",
"createBy": row.create_by or "",
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
}
return result
def _row_to_dict_service_order_product(row) -> Dict[str, Any]:
return {
"id": row.id,
"serviceOrderId": row.service_order_id or "",
"projectCode": row.project_code or "",
"projectName": row.project_name or "",
"cellType": row.cell_type or "",
"specText": row.spec_text or "",
"agreementUnitPrice": float(row.agreement_unit_price) if row.agreement_unit_price else 0,
"buyQty": float(row.buy_qty) if row.buy_qty else 0,
"buyAmount": float(row.buy_amount) if row.buy_amount else 0,
}
def list_channel_service_orders(db, *, page=1, page_size=10, order_type=None,
order_status=None, channel_id=None,
keyword=None, tenant_no=None, user_info=None):
def _filtered():
q0 = db.query(BizChannelServiceOrder).filter(BizChannelServiceOrder.del_flag == 0)
if tenant_no:
q0 = q0.filter(BizChannelServiceOrder.tenant_no == tenant_no)
if order_type:
q0 = q0.filter(BizChannelServiceOrder.order_type == order_type)
if order_status:
# 支持逗号分隔的多个状态
statuses = [s.strip() for s in order_status.split(",") if s.strip()]
if statuses:
q0 = q0.filter(BizChannelServiceOrder.order_status.in_(statuses))
if channel_id:
q0 = q0.filter(BizChannelServiceOrder.channel_id == channel_id)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(BizChannel.channel_name.like(like))
if user_info:
from app.core.data_scope import apply_data_scope
q0 = apply_data_scope(q0, BizChannelServiceOrder, user_info, db, tenant_no)
return q0
total = _filtered().order_by(None).with_entities(func.count(BizChannelServiceOrder.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = _filtered().order_by(BizChannelServiceOrder.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
return [_row_to_dict_service_order(x, db, tenant_no) for x in rows], int(total)
def get_service_order(db, order_id, tenant_no=None):
q = db.query(BizChannelServiceOrder).filter(
and_(BizChannelServiceOrder.id == order_id, BizChannelServiceOrder.del_flag == 0)
)
if tenant_no:
q = q.filter(BizChannelServiceOrder.tenant_no == tenant_no)
return q.first()
def get_service_order_products(db, service_order_id, tenant_no=None):
q = db.query(BizChannelServiceOrderProduct).filter(
and_(BizChannelServiceOrderProduct.service_order_id == service_order_id,
BizChannelServiceOrderProduct.del_flag == 0)
)
if tenant_no:
q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
return q.all()
- Step 2: 追加 create 函数
def create_channel_service_order(
db,
*,
channel_id,
order_type,
channel_name=None,
dept_name=None,
prepay_amount=None,
channel_agreement_id=None,
products=None, # list of dicts: {project_code, project_name, cell_type, spec_text, agreement_unit_price, buy_qty, buy_amount}
tenant_no="zkxw",
remark=None,
user_id=None,
):
"""创建渠道服务购买订单"""
# 1. 校验渠道存在
channel = get_channel(db, channel_id, tenant_no)
if not channel:
raise ValueError("渠道不存在")
channel_name = channel.channel_name
# 2. 确定 order_status
if order_type == "yfk":
if not prepay_amount or float(prepay_amount) <= 0:
raise ValueError("充值金额必须大于 0")
order_status = "dqr"
purchase_total_amount = None
elif order_type == "gmcp":
if not products:
raise ValueError("购买产品订单必须包含产品明细")
order_status = "sxz"
# 校验协议
if not channel_agreement_id:
raise ValueError("购买订单必须关联渠道协议")
agr = db.query(BizChannelAgreement).filter(
and_(BizChannelAgreement.id == channel_agreement_id,
BizChannelAgreement.del_flag == 0)
).first()
if not agr or agr.agreement_status != "sxz":
raise ValueError("渠道协议不存在或未生效")
prepay_amount = None
purchase_total_amount = sum(float(p["buy_amount"]) for p in products)
# 校验余额
account = _get_or_create_channel_account(db, channel_id, tenant_no, user_id)
if float(account.balance_amount) < purchase_total_amount:
raise ValueError(f"渠道余额不足。当前余额: {account.balance_amount}, 需要: {purchase_total_amount}")
else:
raise ValueError(f"不支持的订单类型: {order_type}")
balance_before = None
balance_after = None
# 3. 扣减余额(仅购买订单)
if order_type == "gmcp":
account = _get_or_create_channel_account(db, channel_id, tenant_no, user_id)
balance_before = float(account.balance_amount)
account.balance_amount = float(account.balance_amount) - purchase_total_amount
account.update_by = user_id
account.update_time = datetime.now()
db.add(account)
db.flush()
balance_after = float(account.balance_amount)
# 4. 创建订单主表
order = BizChannelServiceOrder(
id=generate_uuid(),
channel_id=channel_id,
channel_name=channel_name,
dept_name=dept_name or "",
order_type=order_type,
prepay_amount=prepay_amount,
purchase_total_amount=purchase_total_amount,
order_status=order_status,
channel_agreement_id=channel_agreement_id,
balance_before=balance_before,
balance_after=balance_after,
tenant_no=tenant_no,
remark=remark or "",
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(order)
db.flush()
# 5. 创建产品明细 + 更新份数库存(仅购买订单)
if order_type == "gmcp" and products:
for p in products:
product_row = BizChannelServiceOrderProduct(
id=generate_uuid(),
service_order_id=order.id,
project_code=p.get("project_code", ""),
project_name=p.get("project_name", ""),
cell_type=p.get("cell_type", ""),
spec_text=p.get("spec_text", ""),
agreement_unit_price=p.get("agreement_unit_price"),
buy_qty=p.get("buy_qty"),
buy_amount=p.get("buy_amount"),
tenant_no=tenant_no,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(product_row)
# 更新份数库存
# 从协议产品价格表获取 cell_type_code 和 cell_type_name
agr_price = db.query(BizChannelAgreementProductPrice).filter(
and_(
BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
BizChannelAgreementProductPrice.project_code == p.get("project_code"),
BizChannelAgreementProductPrice.spec_text == p.get("spec_text"),
BizChannelAgreementProductPrice.del_flag == 0,
)
).first()
cell_type_code = agr_price.cell_type_code if agr_price else ""
cell_type_name = agr_price.cell_type_name if agr_price else p.get("cell_type", "")
_upsert_channel_product_quota(
db, channel_id,
project_code=p.get("project_code", ""),
project_name=p.get("project_name", ""),
cell_type_code=cell_type_code,
cell_type_name=cell_type_name,
spec_text=p.get("spec_text", ""),
add_qty=float(p.get("buy_qty", 0)),
tenant_no=tenant_no,
user_id=user_id,
)
db.commit()
db.refresh(order)
return order
- Step 3: 追加 update 和 soft_delete 函数
def update_channel_service_order(db, order_id, *, tenant_no=None, user_id=None, **kwargs):
"""修改订单(仅 dqr/sxz 状态可改)"""
order = get_service_order(db, order_id, tenant_no)
if not order:
raise ValueError("订单不存在")
if order.order_status not in ("dqr", "sxz"):
raise ValueError(f"当前状态 {order.order_status} 不允许修改")
# 只允许修改 dept_name, remark
for field in ("dept_name", "remark"):
if field in kwargs and kwargs[field] is not None:
setattr(order, field, kwargs[field])
order.update_by = user_id
order.update_time = datetime.now()
db.add(order)
db.commit()
db.refresh(order)
return order
def soft_delete_service_order(db, order_id, tenant_no=None):
"""软删除订单(仅 dqr 状态可删)"""
order = get_service_order(db, order_id, tenant_no)
if not order:
raise ValueError("订单不存在")
if order.order_status != "dqr":
raise ValueError("只有待确认状态的订单可以删除")
order.del_flag = 1
order.update_time = datetime.now()
db.add(order)
db.commit()
- Step 4: Commit
git add backend/app/crud/pilot_service.py
git commit -m "feat(channel-order): add service order CRUD - list, get, create, update, delete"
Task 5: 后端 CRUD — 状态流转 (confirm/refund/void)
Files:
-
Modify:
backend/app/crud/pilot_service.py(继续追加) -
Step 1: 追加状态流转函数
def confirm_receipt(db, order_id, *, finance_remark=None, finance_confirm_attachments=None,
tenant_no=None, user_id=None):
"""财务确认充值 (dqr → yqr, 余额+)"""
order = get_service_order(db, order_id, tenant_no)
if not order:
raise ValueError("订单不存在")
if order.order_type != "yfk":
raise ValueError("只有充值订单可以进行收款确认")
if order.order_status != "dqr":
raise ValueError(f"当前状态 {order.order_status} 不允许确认收款")
account = _get_or_create_channel_account(db, order.channel_id, tenant_no, user_id)
balance_before = float(account.balance_amount)
account.balance_amount = float(account.balance_amount) + float(order.prepay_amount)
account.update_by = user_id
account.update_time = datetime.now()
db.add(account)
db.flush()
balance_after = float(account.balance_amount)
order.order_status = "yqr"
order.balance_before = balance_before
order.balance_after = balance_after
order.finance_confirm_time = datetime.now()
order.finance_confirm_attachments = finance_confirm_attachments or ""
order.finance_remark = finance_remark or ""
order.update_by = user_id
order.update_time = datetime.now()
db.add(order)
db.commit()
db.refresh(order)
return order
def submit_refund(db, order_id, *, refund_amount=None, refund_remark=None,
refund_attachments=None, tenant_no=None, user_id=None):
"""发起退费 (yqr → tfqrz)"""
order = get_service_order(db, order_id, tenant_no)
if not order:
raise ValueError("订单不存在")
if order.order_type != "yfk":
raise ValueError("只有充值订单可以发起退费")
if order.order_status != "yqr":
raise ValueError(f"当前状态 {order.order_status} 不允许发起退费")
actual_refund = float(refund_amount) if refund_amount else float(order.prepay_amount)
if actual_refund > float(order.prepay_amount):
raise ValueError(f"退费金额不能超过充值金额 {order.prepay_amount}")
order.order_status = "tfqrz"
order.refund_apply_by = user_id
order.refund_apply_time = datetime.now()
order.refund_amount = actual_refund
order.refund_remark = refund_remark or ""
order.refund_attachments = refund_attachments or ""
order.update_by = user_id
order.update_time = datetime.now()
db.add(order)
db.commit()
db.refresh(order)
return order
def confirm_refund(db, order_id, *, refund_finance_remark=None,
refund_finance_confirm_attachments=None, tenant_no=None, user_id=None):
"""财务确认退费 (tfqrz → ytf, 余额-)"""
order = get_service_order(db, order_id, tenant_no)
if not order:
raise ValueError("订单不存在")
if order.order_status != "tfqrz":
raise ValueError(f"当前状态 {order.order_status} 不允许确认退费")
account = _get_or_create_channel_account(db, order.channel_id, tenant_no, user_id)
balance_before = float(account.balance_amount)
account.balance_amount = float(account.balance_amount) - float(order.refund_amount)
account.update_by = user_id
account.update_time = datetime.now()
db.add(account)
db.flush()
balance_after = float(account.balance_amount)
order.order_status = "ytf"
order.balance_before = balance_before
order.balance_after = balance_after
order.refund_finance_confirm_time = datetime.now()
order.refund_finance_confirm_attachments = refund_finance_confirm_attachments or ""
order.refund_finance_remark = refund_finance_remark or ""
order.update_by = user_id
order.update_time = datetime.now()
db.add(order)
db.commit()
db.refresh(order)
return order
def void_service_order(db, order_id, *, tenant_no=None, user_id=None):
"""作废购买订单 (sxz → yzf, 余额+, 份数-)"""
order = get_service_order(db, order_id, tenant_no)
if not order:
raise ValueError("订单不存在")
if order.order_type != "gmcp":
raise ValueError("只有购买订单可以作废")
if order.order_status != "sxz":
raise ValueError(f"当前状态 {order.order_status} 不允许作废")
# 退回余额
account = _get_or_create_channel_account(db, order.channel_id, tenant_no, user_id)
balance_before = float(account.balance_amount)
account.balance_amount = float(account.balance_amount) + float(order.purchase_total_amount)
account.update_by = user_id
account.update_time = datetime.now()
db.add(account)
db.flush()
balance_after = float(account.balance_amount)
# 退回份数
products = get_service_order_products(db, order.id, tenant_no)
for p in products:
# 获取 cell_type_code 从协议价格表
agr_price = db.query(BizChannelAgreementProductPrice).filter(
and_(
BizChannelAgreementProductPrice.channel_agreement_id == order.channel_agreement_id,
BizChannelAgreementProductPrice.project_code == p.project_code,
BizChannelAgreementProductPrice.spec_text == p.spec_text,
BizChannelAgreementProductPrice.del_flag == 0,
)
).first()
cell_type_code = agr_price.cell_type_code if agr_price else ""
_reduce_channel_product_quota(
db, order.channel_id,
project_code=p.project_code,
cell_type_code=cell_type_code,
spec_text=p.spec_text,
reduce_qty=float(p.buy_qty),
user_id=user_id,
)
order.order_status = "yzf"
order.balance_before = balance_before
order.balance_after = balance_after
order.update_by = user_id
order.update_time = datetime.now()
db.add(order)
db.commit()
db.refresh(order)
return order
- Step 2: Commit
git add backend/app/crud/pilot_service.py
git commit -m "feat(channel-order): add status transitions - confirm receipt, refund, void"
Task 6: 后端 API 路由
Files:
-
Modify:
backend/app/api/pilot_service.py -
Step 1: 在文件末尾追加 Pydantic Body 类
# --- 渠道服务购买订单 ---
class ServiceOrderProductItem(BaseModel):
model_config = ConfigDict(populate_by_name=True)
project_code: Optional[str] = None
project_name: Optional[str] = None
cell_type: Optional[str] = None
spec_text: Optional[str] = None
agreement_unit_price: Optional[float] = None
buy_qty: Optional[float] = None
buy_amount: Optional[float] = None
class ServiceOrderCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
channel_id: str
order_type: str # yfk / gmcp
dept_name: Optional[str] = None
prepay_amount: Optional[float] = None
channel_agreement_id: Optional[str] = None
products: Optional[List[ServiceOrderProductItem]] = None
remark: Optional[str] = None
class ServiceOrderUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
dept_name: Optional[str] = None
remark: Optional[str] = None
class FinanceConfirmBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
finance_remark: Optional[str] = None
finance_confirm_attachments: Optional[str] = None
class RefundSubmitBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
refund_amount: Optional[float] = None
refund_remark: Optional[str] = None
refund_attachments: Optional[str] = None
class RefundConfirmBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
refund_finance_remark: Optional[str] = None
refund_finance_confirm_attachments: Optional[str] = None
- Step 2: 追加 API 端点
@router.get("/channel-service-orders")
async def list_service_orders(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
orderType: Optional[str] = Query(None, alias="orderType"),
orderStatus: Optional[str] = Query(None, alias="orderStatus"),
channelId: Optional[str] = Query(None, alias="channelId"),
keyword: Optional[str] = None,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
items, total = ps_svc.list_channel_service_orders(
db, page=page, page_size=pageSize, order_type=orderType,
order_status=orderStatus, channel_id=channelId,
keyword=keyword, tenant_no=tenant_no, user_info=user,
)
tp = ps_svc.total_pages(total, pageSize)
return success_response(
{"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}
)
@router.get("/channel-service-orders/{order_id}")
async def get_service_order_detail(
order_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
order = ps_svc.get_service_order(db, order_id, tenant_no)
if not order:
return error_response(404, "订单不存在")
result = ps_svc._row_to_dict_service_order(order, db, tenant_no)
# 加载产品明细
products = ps_svc.get_service_order_products(db, order_id, tenant_no)
result["products"] = [ps_svc._row_to_dict_service_order_product(p) for p in products]
return success_response(result)
@router.post("/channel-service-orders")
async def create_service_order(
body: ServiceOrderCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
products_data = None
if body.products:
products_data = [p.model_dump() for p in body.products]
obj = ps_svc.create_channel_service_order(
db,
channel_id=body.channel_id,
order_type=body.order_type,
dept_name=body.dept_name,
prepay_amount=body.prepay_amount,
channel_agreement_id=body.channel_agreement_id,
products=products_data,
tenant_no=tenant_no,
remark=body.remark,
user_id=user.get("user_id"),
)
result = ps_svc._row_to_dict_service_order(obj, db, tenant_no)
prods = ps_svc.get_service_order_products(db, obj.id, tenant_no)
result["products"] = [ps_svc._row_to_dict_service_order_product(p) for p in prods]
return success_response(result, message="创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/channel-service-orders/{order_id}")
async def update_service_order(
order_id: str,
body: ServiceOrderUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
payload = body.model_dump(exclude_unset=True, by_alias=False)
obj = ps_svc.update_channel_service_order(
db, order_id, tenant_no=tenant_no, user_id=user.get("user_id"), **payload,
)
return success_response(
ps_svc._row_to_dict_service_order(obj, db, tenant_no),
message="更新成功",
)
except ValueError as e:
return error_response(400, str(e))
@router.delete("/channel-service-orders/{order_id}")
async def delete_service_order(
order_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
ps_svc.soft_delete_service_order(db, order_id, tenant_no=tenant_no)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/channel-service-orders/{order_id}/confirm-receipt")
async def confirm_receipt_api(
order_id: str,
body: FinanceConfirmBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
obj = ps_svc.confirm_receipt(
db, order_id,
finance_remark=body.finance_remark,
finance_confirm_attachments=body.finance_confirm_attachments,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
return success_response(
ps_svc._row_to_dict_service_order(obj, db, tenant_no),
message="确认收款成功",
)
except ValueError as e:
return error_response(400, str(e))
@router.post("/channel-service-orders/{order_id}/submit-refund")
async def submit_refund_api(
order_id: str,
body: RefundSubmitBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
obj = ps_svc.submit_refund(
db, order_id,
refund_amount=body.refund_amount,
refund_remark=body.refund_remark,
refund_attachments=body.refund_attachments,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
return success_response(
ps_svc._row_to_dict_service_order(obj, db, tenant_no),
message="退费申请已提交",
)
except ValueError as e:
return error_response(400, str(e))
@router.post("/channel-service-orders/{order_id}/confirm-refund")
async def confirm_refund_api(
order_id: str,
body: RefundConfirmBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
obj = ps_svc.confirm_refund(
db, order_id,
refund_finance_remark=body.refund_finance_remark,
refund_finance_confirm_attachments=body.refund_finance_confirm_attachments,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
return success_response(
ps_svc._row_to_dict_service_order(obj, db, tenant_no),
message="退费确认成功",
)
except ValueError as e:
return error_response(400, str(e))
@router.put("/channel-service-orders/{order_id}/void")
async def void_service_order_api(
order_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
obj = ps_svc.void_service_order(
db, order_id,
tenant_no=tenant_no,
user_id=user.get("user_id"),
)
return success_response(
ps_svc._row_to_dict_service_order(obj, db, tenant_no),
message="订单已作废",
)
except ValueError as e:
return error_response(400, str(e))
@router.get("/channels/{channel_id}/account")
async def get_channel_account_api(
channel_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
account = ps_svc.get_channel_account(db, channel_id, tenant_no)
if not account:
return success_response({"channelId": channel_id, "balanceAmount": 0.0})
return success_response(ps_svc._row_to_dict_channel_account(account))
@router.get("/channels/{channel_id}/product-quotas")
async def list_product_quotas_api(
channel_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
items = ps_svc.list_channel_product_quotas(db, channel_id, tenant_no)
return success_response(items)
- Step 3: 验证后端启动无报错
Run: cd backend && source venv/bin/activate && python -c "from app.api.pilot_service import router; print('API routes OK')"
- Step 4: Commit
git add backend/app/api/pilot_service.py
git commit -m "feat(channel-order): add API endpoints for service orders, finance confirm, account, quotas"
Task 7: 前端 API 封装 & 状态标签
Files:
-
Modify:
web-admin/src/api/admin.ts -
Modify:
web-admin/src/utils/status-tag-registry.ts -
Step 1: 在
admin.ts末尾追加 API 函数
// ========== 渠道服务购买订单 ==========
export function fetchChannelServiceOrderList(params: any) {
return request({ url: '/admin/channel-service-orders', method: 'get', params })
}
export function fetchChannelServiceOrderDetail(id: string) {
return request({ url: `/admin/channel-service-orders/${id}`, method: 'get' })
}
export function fetchCreateChannelServiceOrder(data: any) {
return request({ url: '/admin/channel-service-orders', method: 'post', data })
}
export function fetchUpdateChannelServiceOrder(id: string, data: any) {
return request({ url: `/admin/channel-service-orders/${id}`, method: 'put', data })
}
export function fetchDeleteChannelServiceOrder(id: string) {
return request({ url: `/admin/channel-service-orders/${id}`, method: 'delete' })
}
export function fetchConfirmReceipt(id: string, data: any) {
return request({ url: `/admin/channel-service-orders/${id}/confirm-receipt`, method: 'post', data })
}
export function fetchSubmitRefund(id: string, data: any) {
return request({ url: `/admin/channel-service-orders/${id}/submit-refund`, method: 'post', data })
}
export function fetchConfirmRefund(id: string, data: any) {
return request({ url: `/admin/channel-service-orders/${id}/confirm-refund`, method: 'post', data })
}
export function fetchVoidChannelServiceOrder(id: string) {
return request({ url: `/admin/channel-service-orders/${id}/void`, method: 'put' })
}
export function fetchChannelAccount(channelId: string) {
return request({ url: `/admin/channels/${channelId}/account`, method: 'get' })
}
export function fetchChannelProductQuotas(channelId: string) {
return request({ url: `/admin/channels/${channelId}/product-quotas`, method: 'get' })
}
- Step 2: 在
status-tag-registry.ts中追加状态定义
在 agreementStatus 之后追加:
serviceOrderType: {
yfk: { label: '充值', tagType: 'info' },
gmcp: { label: '购买产品', tagType: 'primary' },
},
serviceOrderStatus: {
dqr: { label: '待确认', tagType: 'info' },
yqr: { label: '已确认', tagType: 'success' },
tfqrz: { label: '退费确认中', tagType: 'warning' },
ytf: { label: '已退费', tagType: 'danger' },
sxz: { label: '生效中', tagType: 'success' },
yzf: { label: '已作废', tagType: 'danger' },
},
同时在 statusMap 导出对象中注册这两个新映射。
- Step 3: Commit
git add web-admin/src/api/admin.ts web-admin/src/utils/status-tag-registry.ts
git commit -m "feat(channel-order): add frontend API functions and status tag registry"
Task 8: 前端 — 渠道服务购买订单管理页
Files:
-
Create:
web-admin/src/views/pilot-service/channel-service-order/index.vue -
Step 1: 创建订单列表页
参考 system/order/index.vue 复杂列表模式。核心功能:
- 搜索栏:订单类型下拉、订单状态下拉、渠道名称搜索、创建时间范围
- 列表:el-table 展示订单编号、渠道名称、订单类型(Tag)、金额、状态(Tag)、创建时间、操作
- 操作按钮按状态动态显示
- 新增/编辑弹窗:选择渠道 → 选择订单类型 → 充值/购买模式切换
- 购买模式:选择协议 → 添加产品 → 修改单价 → 计算金额 → 余额校验
- 查看弹窗:展示订单详情 + 产品明细 + 财务确认信息
- 退费弹窗:填写退费金额/备注/附件
- 作废确认弹窗
参考模板: src/views/system/order/index.vue 的列表+弹窗结构。
参考渠道协议选择: src/views/pilot-service/channel/index.vue 中协议管理的模式。
状态 Tag 使用: <el-tag :type="getStatusTag('serviceOrderStatus', row.orderStatus)?.tagType" size="small">{{ getStatusTag('serviceOrderStatus', row.orderStatus)?.label }}</el-tag>
由于此页面逻辑较复杂(~1500 行),实现时遵循以下关键约束:
-
使用
label-position="top"表单布局 -
弹窗
.dialog-form-layout样式 -
表格操作列
fixed="right"min-width ≥ 260px -
表格列使用
min-width不使用width -
状态/类型字段使用
<el-tag>展示 -
Fresh Mint 色彩规范(
--primary #10b981) -
Step 2: Commit
git add web-admin/src/views/pilot-service/channel-service-order/
git commit -m "feat(channel-order): add channel service order management page"
Task 9: 前端 — 财务确认页
Files:
-
Create:
web-admin/src/views/pilot-service/channel-service-order-finance/index.vue -
Step 1: 创建财务确认页
参考 cell-storage/payment-confirm/index.vue 的双 Tab 模式。
- Tab 1:待确认 — 列表显示
orderStatus IN ('dqr', 'tfqrz')的订单- dqr 订单 → 「确认收款」按钮 → 弹窗(只读订单信息 + 财务备注 textarea + 附件上传卡片)
- tfqrz 订单 → 「确认退费」按钮 → 弹窗(只读订单+退费信息 + 退费确认备注 + 退费确认附件)
- Tab 2:已确认 — 列表显示
orderStatus IN ('yqr', 'ytf')的订单- 查看详情弹窗
参考模板: src/views/cell-storage/payment-confirm/index.vue
附件上传: 使用卡片化上传模式(140×96px dashed border card),参考 ProtocolFormDialog.vue 的附件上传。
- Step 2: Commit
git add web-admin/src/views/pilot-service/channel-service-order-finance/
git commit -m "feat(channel-order): add financial confirmation page"
Task 10: 前端 — 渠道信息页扩展(账户 Tab)
Files:
-
Modify:
web-admin/src/views/pilot-service/channel/ChannelViewDialog.vue -
Step 1: 在查看弹窗中新增「账户信息」Tab
在 ChannelViewDialog.vue 的 Tab 列表中追加「账户信息」Tab:
-
展示当前余额(大字数字展示)
-
展示产品份数库存列表(el-table: 项目名称、细胞类型、规格、总份数、已使用、剩余份数)
-
数据通过
fetchChannelAccount(channelId)和fetchChannelProductQuotas(channelId)获取 -
Step 2: Commit
git add web-admin/src/views/pilot-service/channel/ChannelViewDialog.vue
git commit -m "feat(channel-order): add account info tab to channel view dialog"
Task 11: 集成验证
- Step 1: 启动后端服务
Run: cd backend && source venv/bin/activate && python -m uvicorn app.main:app --reload --port 8000
- Step 2: 访问 Swagger 文档验证 API
打开 http://127.0.0.1:8000/docs,检查以下端点是否出现:
-
GET
/api/web/admin/channel-service-orders -
POST
/api/web/admin/channel-service-orders -
POST
/api/web/admin/channel-service-orders/{id}/confirm-receipt -
POST
/api/web/admin/channel-service-orders/{id}/submit-refund -
POST
/api/web/admin/channel-service-orders/{id}/confirm-refund -
PUT
/api/web/admin/channel-service-orders/{id}/void -
GET
/api/web/admin/channels/{channelId}/account -
GET
/api/web/admin/channels/{channelId}/product-quotas -
Step 3: 启动前端开发服务器
Run: cd web-admin && pnpm dev
- Step 4: 访问页面验证功能
打开浏览器:
- 渠道服务购买订单列表页
- 财务确认页
- 渠道详情页的账户信息 Tab
测试场景:
- 创建充值订单 → 财务确认 → 余额增加
- 创建购买订单 → 选择协议 → 添加产品 → 扣余额 → 查看份数库存
- 作废购买订单 → 余额退回 → 份数退回
- 充值订单发起退费 → 财务确认退费 → 余额减少
- Step 5: Final Commit
git add -A
git commit -m "feat(channel-order): complete channel service order feature with balance and quota management"