This commit is contained in:
zlwu committed 2026-05-24 23:04:33 +08:00
1 parent 13e4c9530a
commit 575e63e7e8
62 files changed
+28759 -9636

No files matched your search

@@ -0,0 +1,633 @@
# 渠道服务订单多次操作流水模型 实施计划
> **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)管汇总,新增操作流水表(biz_channel_service_order_txn)管每笔操作的明细和财务确认流程,产品持仓表(biz_channel_service_order_product)改造为追踪累计购买/退订/当前持有份数。
**Tech Stack:** Python 3.9 / FastAPI / SQLAlchemy / Pydantic v2 / MySQL 5.7 / Vue 3 / Element Plus / TypeScript
**Design Spec:** `docs/superpowers/specs/2026-05-24-channel-service-order-multi-txn-design.md`
---
## 文件结构
### 新增文件
- `docs/sql/channel-service-order-txn-migration.sql` — 数据库迁移 SQL
- `backend/app/models/channel_service_order_txn.py` — 流水 Model(或合并到 models.py)
### 修改文件
- `backend/app/models/models.py` — 新增 BizChannelServiceOrderTxn model,改造 BizChannelServiceOrder/BizChannelServiceOrderProduct
- `backend/app/crud/pilot_service.py` — 新增流水 CRUD + 改造订单 CRUD
- `backend/app/api/pilot_service.py` — 新增流水 API + Pydantic schemas
- `web-admin/src/api/admin.ts` — 新增流水相关 API 调用函数
- `web-admin/src/views/pilot-service/channel-service-order/index.vue` — 订单管理页改造
- `web-admin/src/views/pilot-service/channel-service-order-finance/index.vue` — 财务确认页改造
---
## Task 1: 数据库迁移 — 流水表 + 主表改造 + 产品表改造
**Files:**
- Create: `docs/sql/channel-service-order-txn-migration.sql`
- [ ] **Step 1: 编写迁移 SQL**
```sql
-- ============================================================
-- 渠道服务订单:多次操作流水模型迁移
-- ============================================================
-- 1. 新增操作流水表
CREATE TABLE `biz_channel_service_order_txn` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`service_order_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '关联订单ID',
`txn_type` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'recharge/refund/purchase/unsubscribe',
`amount` decimal(18,2) DEFAULT NULL COMMENT '本次操作金额',
`txn_status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'dsx' COMMENT 'dsx待生效 dqr待财务确认 yqr已确认 yzf已作废',
`products_json` text COLLATE utf8mb4_unicode_ci COMMENT '产品明细JSON(purchase/unsubscribe类型)',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`finance_submit_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '发起确认人ID',
`finance_submit_time` datetime DEFAULT NULL COMMENT '发起确认时间',
`finance_submit_remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '发起确认备注',
`finance_submit_attachments` text COLLATE utf8mb4_unicode_ci COMMENT '发起确认附件',
`finance_confirm_time` datetime DEFAULT NULL COMMENT '财务确认时间',
`finance_confirm_attachments` text COLLATE utf8mb4_unicode_ci COMMENT '财务确认附件',
`finance_remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '财务确认备注',
`balance_before` decimal(18,2) DEFAULT NULL COMMENT '操作前余额(yfk类型)',
`balance_after` decimal(18,2) DEFAULT NULL COMMENT '操作后余额(yfk类型)',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci 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 '删除标记',
PRIMARY KEY (`id`),
KEY `idx_txn_order` (`service_order_id`, `del_flag`),
KEY `idx_txn_status` (`txn_status`, `tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道服务订单操作流水';
-- 2. 订单主表新增汇总字段
ALTER TABLE `biz_channel_service_order`
ADD COLUMN `total_recharge_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '累计充值总额' AFTER `channel_agreement_id`,
ADD COLUMN `total_refund_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '累计退款总额' AFTER `total_recharge_amount`,
ADD COLUMN `current_balance` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '当前余额' AFTER `total_refund_amount`;
-- 3. 产品持仓表新增字段
ALTER TABLE `biz_channel_service_order_product`
ADD COLUMN `total_buy_qty` decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '累计购买数量' AFTER `buy_amount`,
ADD COLUMN `total_unsubscribe_qty` decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '累计退订数量' AFTER `total_buy_qty`,
ADD COLUMN `current_qty` decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '当前持有数量' AFTER `total_unsubscribe_qty`,
ADD COLUMN `total_unsubscribe_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '累计退订金额' AFTER `current_qty`;
-- 4. 迁移现有订单数据到流水表(一次性,保留历史数据)
-- 注意:仅迁移有操作记录的订单(有 finance_submit_time 或 refund_apply_time 的)
INSERT INTO `biz_channel_service_order_txn` (
`id`, `service_order_id`, `txn_type`, `amount`, `txn_status`,
`remark`,
`finance_submit_by`, `finance_submit_time`, `finance_submit_remark`, `finance_submit_attachments`,
`finance_confirm_time`, `finance_confirm_attachments`, `finance_remark`,
`balance_before`, `balance_after`,
`tenant_no`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
UUID_SHORT() AS `id`,
o.`id` AS `service_order_id`,
'recharge' AS `txn_type`,
o.`prepay_amount` AS `amount`,
CASE
WHEN o.`finance_confirm_time` IS NOT NULL THEN 'yqr'
WHEN o.`finance_submit_time` IS NOT NULL THEN 'dqr'
ELSE 'dsx'
END AS `txn_status`,
o.`remark`,
o.`finance_submit_by`, o.`finance_submit_time`, o.`finance_submit_remark`, o.`finance_submit_attachments`,
o.`finance_confirm_time`, o.`finance_confirm_attachments`, o.`finance_remark`,
o.`balance_before`, o.`balance_after`,
o.`tenant_no`, o.`create_by`, o.`create_dept_id`, o.`create_time`, o.`update_by`, o.`update_time`, o.`del_flag`
FROM `biz_channel_service_order` o
WHERE o.`finance_submit_time` IS NOT NULL OR o.`prepay_amount` IS NOT NULL;
-- 5. 迁移退款记录到流水表
INSERT INTO `biz_channel_service_order_txn` (
`id`, `service_order_id`, `txn_type`, `amount`, `txn_status`,
`remark`,
`finance_submit_by`, `finance_submit_time`, `finance_submit_remark`, `finance_submit_attachments`,
`finance_confirm_time`, `finance_confirm_attachments`, `finance_remark`,
`tenant_no`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
UUID_SHORT() AS `id`,
o.`id` AS `service_order_id`,
'refund' AS `txn_type`,
o.`refund_amount` AS `amount`,
CASE
WHEN o.`refund_finance_confirm_time` IS NOT NULL THEN 'yqr'
ELSE 'dqr'
END AS `txn_status`,
o.`refund_remark` AS `remark`,
o.`refund_apply_by` AS `finance_submit_by`,
o.`refund_apply_time` AS `finance_submit_time`,
o.`refund_remark` AS `finance_submit_remark`,
o.`refund_attachments` AS `finance_submit_attachments`,
o.`refund_finance_confirm_time` AS `finance_confirm_time`,
o.`refund_finance_confirm_attachments` AS `finance_confirm_attachments`,
o.`refund_finance_remark` AS `finance_remark`,
o.`tenant_no`, o.`refund_apply_by` AS `create_by`, o.`create_dept_id`,
COALESCE(o.`refund_apply_time`, o.`update_time`) AS `create_time`,
o.`update_by`, o.`update_time`, o.`del_flag`
FROM `biz_channel_service_order` o
WHERE o.`refund_amount` IS NOT NULL AND o.`refund_amount` > 0;
-- 6. 回填主表汇总字段
UPDATE `biz_channel_service_order` o
SET
`total_recharge_amount` = COALESCE(o.`prepay_amount`, 0),
`total_refund_amount` = COALESCE(o.`refund_amount`, 0),
`current_balance` = COALESCE(o.`prepay_amount`, 0) - COALESCE(o.`refund_amount`, 0)
WHERE o.`order_type` = 'yfk' AND o.`prepay_amount` IS NOT NULL;
-- 7. 回填产品持仓表的累计字段
UPDATE `biz_channel_service_order_product` p
SET
`total_buy_qty` = COALESCE(p.`buy_qty`, 0),
`current_qty` = COALESCE(p.`buy_qty`, 0);
```
- [ ] **Step 2: 在数据库执行迁移**
Run: 手动在 MySQL 中执行上述 SQL,或逐步执行确认无误。
---
## Task 2: 后端 Model — 新增流水 Model + 改造主表/产品 Model
**Files:**
- Modify: `backend/app/models/models.py` (line ~1762, BizChannelServiceOrder class)
- [ ] **Step 1: 在 models.py 中 BizChannelServiceOrder 类新增字段**
在 `BizChannelServiceOrder` 类的 `channel_agreement_id` 字段后面添加三个汇总字段:
```python
total_recharge_amount = Column(Numeric(18, 2), nullable=False, default=0, comment="累计充值总额")
total_refund_amount = Column(Numeric(18, 2), nullable=False, default=0, comment="累计退款总额")
current_balance = Column(Numeric(18, 2), nullable=False, default=0, comment="当前余额")
```
- [ ] **Step 2: 在 BizChannelServiceOrderProduct 类新增字段**
在 `buy_amount` 字段后面添加:
```python
total_buy_qty = Column(Numeric(18, 4), nullable=False, default=0, comment="累计购买数量")
total_unsubscribe_qty = Column(Numeric(18, 4), nullable=False, default=0, comment="累计退订数量")
current_qty = Column(Numeric(18, 4), nullable=False, default=0, comment="当前持有数量")
total_unsubscribe_amount = Column(Numeric(18, 2), nullable=False, default=0, comment="累计退订金额")
```
- [ ] **Step 3: 在 BizChannelServiceOrderProduct 类后面新增 BizChannelServiceOrderTxn 类**
```python
class BizChannelServiceOrderTxn(Base):
"""渠道服务订单操作流水 biz_channel_service_order_txn"""
__tablename__ = "biz_channel_service_order_txn"
id = Column(String(64), primary_key=True, comment="主键ID")
service_order_id = Column(String(64), nullable=False, comment="关联订单ID")
txn_type = Column(String(16), nullable=False, comment="recharge/refund/purchase/unsubscribe")
amount = Column(Numeric(18, 2), nullable=True, comment="本次操作金额")
txn_status = Column(String(16), nullable=False, default="dsx", comment="dsx待生效 dqr待财务确认 yqr已确认 yzf已作废")
products_json = Column(Text, nullable=True, comment="产品明细JSON(purchase/unsubscribe类型)")
remark = Column(String(512), nullable=True, comment="备注")
finance_submit_by = Column(String(64), nullable=True, comment="发起确认人ID")
finance_submit_time = Column(DateTime, nullable=True, comment="发起确认时间")
finance_submit_remark = Column(String(512), nullable=True, comment="发起确认备注")
finance_submit_attachments = Column(Text, nullable=True, comment="发起确认附件")
finance_confirm_time = Column(DateTime, nullable=True, comment="财务确认时间")
finance_confirm_attachments = Column(Text, nullable=True, comment="财务确认附件")
finance_remark = Column(String(512), nullable=True, comment="财务确认备注")
balance_before = Column(Numeric(18, 2), nullable=True, comment="操作前余额")
balance_after = Column(Numeric(18, 2), nullable=True, comment="操作后余额")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark_col = Column("remark", 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="删除标记")
```
注意:Model 中 `remark` 字段和表里的 `remark` 列名冲突(流水表有两个 remark 用途)—— 实际上流水表只有一个 `remark` 列。Model 中只保留一个 `remark` 即可,去掉上面的 `remark_col`,直接用 `remark`。
- [ ] **Step 4: 验证 import**
确保 `models.py` 文件头部已有 `from sqlalchemy import ... Column, String, Numeric, Text, DateTime, SmallInteger, Integer` 等导入。
---
## Task 3: 后端 CRUD — 流水 CRUD + 订单 CRUD 改造
**Files:**
- Modify: `backend/app/crud/pilot_service.py` (line ~2513+)
- [ ] **Step 1: 新增流水 _row_to_dict 函数**
在 `_row_to_dict_service_order_product` 函数后面添加:
```python
def _row_to_dict_service_order_txn(row, db=None, tenant_no=None) -> Dict[str, Any]:
tn = tenant_no or row.tenant_no
d = {
"id": row.id,
"serviceOrderId": row.service_order_id or "",
"txnType": row.txn_type or "",
"amount": float(row.amount) if row.amount else 0,
"txnStatus": row.txn_status or "",
"productsJson": row.products_json or "",
"remark": row.remark or "",
"financeSubmitBy": row.finance_submit_by or "",
"financeSubmitTime": row.finance_submit_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_submit_time else "",
"financeSubmitRemark": row.finance_submit_remark or "",
"financeSubmitAttachments": row.finance_submit_attachments or "",
"financeSubmitAttachmentFiles": urls_to_attachment_list(row.finance_submit_attachments, db, tn) if db and row.finance_submit_attachments else [],
"financeConfirmTime": row.finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_confirm_time else "",
"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 "",
"balanceBefore": float(row.balance_before) if row.balance_before else 0,
"balanceAfter": float(row.balance_after) if row.balance_after else 0,
"tenantNo": row.tenant_no 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 "",
}
if row.products_json:
try:
d["products"] = json.loads(row.products_json)
except Exception:
d["products"] = []
else:
d["products"] = []
return d
```
- [ ] **Step 2: 改造 _row_to_dict_service_order,新增汇总字段、移除旧字段**
将 `prepayAmount`、`purchaseTotalAmount` 等保留但标记为兼容字段(从汇总计算),新增:
- `totalRechargeAmount`
- `totalRefundAmount`
- `currentBalance`
- `transactions`(列表,由调用方填充)
- `products`(列表,由调用方填充)
同时在详情查询时附带流水列表和产品持仓列表。
- [ ] **Step 3: 新增流水 CRUD 函数**
```python
def list_service_order_txns(db, *, service_order_id=None, txn_status=None, txn_type=None, page=1, page_size=20, tenant_no=None):
"""查询操作流水列表"""
...
def get_service_order_txn(db, txn_id, *, tenant_no=None):
"""获取单条流水"""
...
def create_recharge_txn(db, order_id, *, amount, remark=None, tenant_no=None, user_id=None):
"""创建充值流水(yfk订单)"""
...
def create_refund_txn(db, order_id, *, amount, remark=None, tenant_no=None, user_id=None):
"""创建退款流水(yfk订单),校验 amount <= current_balance"""
...
def create_purchase_txn(db, order_id, *, products, remark=None, tenant_no=None, user_id=None):
"""创建购买产品流水(gmcp订单),同时创建/更新产品持仓"""
...
def create_unsubscribe_txn(db, order_id, *, products, remark=None, tenant_no=None, user_id=None):
"""创建退订产品流水(gmcp订单),校验退订份数 <= current_qty"""
...
def submit_txn_finance(db, txn_id, *, finance_submit_remark=None, finance_submit_attachments=None, tenant_no=None, user_id=None):
"""发起流水财务确认:dsx → dqr"""
...
def confirm_txn(db, txn_id, *, finance_remark=None, finance_confirm_attachments=None, tenant_no=None, user_id=None):
"""财务确认流水:dqr → yqr,更新主表汇总和产品持仓"""
...
def void_txn(db, txn_id, *, tenant_no=None, user_id=None):
"""作废流水:dsx → yzf"""
...
```
- [ ] **Step 4: 改造 _row_to_dict_service_order_product,新增持仓字段**
在返回字典中新增:
```python
"totalBuyQty": float(row.total_buy_qty) if row.total_buy_qty else 0,
"totalUnsubscribeQty": float(row.total_unsubscribe_qty) if row.total_unsubscribe_qty else 0,
"currentQty": float(row.current_qty) if row.current_qty else 0,
"totalUnsubscribeAmount": float(row.total_unsubscribe_amount) if row.total_unsubscribe_amount else 0,
```
---
## Task 4: 后端 API — 流水接口 + Pydantic Schemas
**Files:**
- Modify: `backend/app/api/pilot_service.py` (line ~1552+)
- [ ] **Step 1: 新增 Pydantic Schemas**
在现有 `RefundConfirmBody` 后面添加:
```python
class TxnRechargeBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
amount: float = Field(..., gt=0, alias="amount")
remark: Optional[str] = Field(None, alias="remark")
class TxnRefundBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
amount: float = Field(..., gt=0, alias="amount")
remark: Optional[str] = Field(None, alias="remark")
class TxnProductItem(BaseModel):
model_config = ConfigDict(populate_by_name=True)
project_code: Optional[str] = Field(None, alias="projectCode")
project_name: Optional[str] = Field(None, alias="projectName")
cell_type: Optional[str] = Field(None, alias="cellType")
spec_text: Optional[str] = Field(None, alias="specText")
agreement_unit_price: Optional[float] = Field(None, alias="agreementUnitPrice")
qty: Optional[float] = Field(None, alias="qty")
amount: Optional[float] = Field(None, alias="amount")
class TxnPurchaseBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
products: List[TxnProductItem] = Field(..., alias="products")
remark: Optional[str] = Field(None, alias="remark")
class TxnUnsubscribeBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
products: List[TxnProductItem] = Field(..., alias="products")
remark: Optional[str] = Field(None, alias="remark")
class TxnFinanceSubmitBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
finance_submit_remark: Optional[str] = Field(None, alias="financeSubmitRemark")
finance_submit_attachments: Optional[str] = Field(None, alias="financeSubmitAttachments")
class TxnFinanceConfirmBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
finance_remark: Optional[str] = Field(None, alias="financeRemark")
finance_confirm_attachments: Optional[str] = Field(None, alias="financeConfirmAttachments")
```
- [ ] **Step 2: 新增流水 API 路由**
```python
@router.get("/channel-service-order-txns")
async def list_txns(...) # 列表查询,支持 status/type/order_id 筛选
@router.get("/channel-service-order-txns/{txn_id}")
async def get_txn_detail(...) # 流水详情
@router.post("/channel-service-orders/{order_id}/recharge")
async def create_recharge(...) # 创建充值流水
@router.post("/channel-service-orders/{order_id}/refund")
async def create_refund(...) # 创建退款流水
@router.post("/channel-service-orders/{order_id}/purchase")
async def create_purchase(...) # 创建购买流水
@router.post("/channel-service-orders/{order_id}/unsubscribe")
async def create_unsubscribe(...) # 创建退订流水
@router.post("/channel-service-order-txns/{txn_id}/submit-finance")
async def submit_txn_finance(...) # 发起财务确认
@router.post("/channel-service-order-txns/{txn_id}/confirm")
async def confirm_txn(...) # 财务确认
@router.put("/channel-service-order-txns/{txn_id}/void")
async def void_txn(...) # 作废流水
```
- [ ] **Step 3: 改造订单详情 API**
修改 `get_channel_service_order_detail` 接口,在返回数据中附带:
- `transactions`: 该订单的所有流水列表
- `products`: 该订单的产品持仓列表(gmcp)
---
## Task 5: 前端 API — 新增流水相关调用函数
**Files:**
- Modify: `web-admin/src/api/admin.ts` (line ~1900+)
- [ ] **Step 1: 新增流水 API 函数**
在现有 `fetchVoidServiceOrder` 后面添加:
```typescript
// ========== 渠道服务订单操作流水 ==========
/** 流水列表 */
export function fetchServiceOrderTxnList(params?: Record<string, any>) {
return request.get({ url: '/web/admin/channel-service-order-txns', params })
}
/** 流水详情 */
export function fetchServiceOrderTxnDetail(txnId: string) {
return request.get({ url: `/web/admin/channel-service-order-txns/${txnId}` })
}
/** 发起充值 */
export function fetchCreateRechargeTxn(orderId: string, data: Record<string, any>) {
return request.post({ url: `/web/admin/channel-service-orders/${orderId}/recharge`, params: data })
}
/** 申请退款 */
export function fetchCreateRefundTxn(orderId: string, data: Record<string, any>) {
return request.post({ url: `/web/admin/channel-service-orders/${orderId}/refund`, params: data })
}
/** 购买产品 */
export function fetchCreatePurchaseTxn(orderId: string, data: Record<string, any>) {
return request.post({ url: `/web/admin/channel-service-orders/${orderId}/purchase`, params: data })
}
/** 退订产品 */
export function fetchCreateUnsubscribeTxn(orderId: string, data: Record<string, any>) {
return request.post({ url: `/web/admin/channel-service-orders/${orderId}/unsubscribe`, params: data })
}
/** 流水发起财务确认 */
export function fetchSubmitTxnFinance(txnId: string, data: Record<string, any>) {
return request.post({ url: `/web/admin/channel-service-order-txns/${txnId}/submit-finance`, params: data })
}
/** 流水财务确认 */
export function fetchConfirmTxn(txnId: string, data: Record<string, any>) {
return request.post({ url: `/web/admin/channel-service-order-txns/${txnId}/confirm`, params: data })
}
/** 作废流水 */
export function fetchVoidTxn(txnId: string) {
return request.put({ url: `/web/admin/channel-service-order-txns/${txnId}/void` })
}
```
- [ ] **Step 2: 保留旧 API 函数但标记废弃**
不删除 `fetchSubmitServiceOrderFinanceConfirm`、`fetchConfirmServiceOrderReceipt`、`fetchSubmitServiceOrderRefund`、`fetchConfirmServiceOrderRefund`,保持向后兼容,但前端页面不再调用。
---
## Task 6: 前端订单管理页改造
**Files:**
- Modify: `web-admin/src/views/pilot-service/channel-service-order/index.vue`
这是改动最大的文件,核心变更:
- [ ] **Step 1: 更新 import**
新增导入流水相关 API 函数:`fetchCreateRechargeTxn`, `fetchCreateRefundTxn`, `fetchCreatePurchaseTxn`, `fetchCreateUnsubscribeTxn`, `fetchSubmitTxnFinance`,移除旧的 `fetchSubmitServiceOrderFinanceConfirm`, `fetchSubmitServiceOrderRefund` 调用。
- [ ] **Step 2: 更新操作按钮**
将操作列的按钮替换为:
- yfk + dsx → 编辑、删除、**发起充值**
- yfk + sxz → **发起充值**、**申请退款**
- gmcp + dsx → 编辑、删除、**购买产品**
- gmcp + sxz → **购买产品**、**退订产品**
- 任意 + yzf → 仅查看
- [ ] **Step 3: 改造详情弹窗(SystemViewDialog)**
在 viewSections computed 中:
- 基本信息section 新增 `currentBalance`(yfk)或产品持仓汇总(gmcp)
- 移除旧的"发起财务确认""财务确认信息""退款信息"平铺 sections
- 新增"操作记录"section,用 inner-table 展示流水列表(类型Tag、金额、状态Tag、时间)
- gmcp 类型新增"产品持仓"section,显示各产品当前持有份数
- [ ] **Step 4: 替换弹窗**
移除旧的"发起财务确认"弹窗和"申请退款"弹窗,新增:
- **发起充值弹窗**:金额输入、备注、凭证上传
- **申请退款弹窗**:金额输入(max=当前余额)、备注、凭证上传
- **购买产品弹窗**:选项目/规格、输入份数、自动算金额(类似现有的新增订单弹窗中产品明细部分)
- **退订产品弹窗**:选已持有项目(下拉,显示当前份数)、输入退订份数(≤持有量)、自动算退款金额
- [ ] **Step 5: 操作按钮点击后,可选择是否立即发起财务确认**
创建流水后,弹出提示"是否立即发起财务确认?",用户点确认则调用 `fetchSubmitTxnFinance`。
---
## Task 7: 前端财务确认页改造
**Files:**
- Modify: `web-admin/src/views/pilot-service/channel-service-order-finance/index.vue`
- [ ] **Step 1: 改为查流水表**
将 `fetchChannelServiceOrderList` 替换为 `fetchServiceOrderTxnList`,参数传递 `txnStatus=dqr`(待处理)或 `txnStatus=yqr`(已处理)。
- [ ] **Step 2: 表格列调整**
列表表格列改为:
- 序号、渠道名称、订单类型Tag、流水类型Tag(充值/退款/购买/退订)、金额、状态Tag、提交人、提交时间、操作
流水类型标签映射:
```typescript
const txnTypeLabel = (t: string) => {
const map: Record<string, string> = { recharge: '充值', refund: '退款', purchase: '购买产品', unsubscribe: '退订产品' }
return map[t] || t
}
```
- [ ] **Step 3: Tab 切换调整**
已有两 tab(待处理/已处理),改为:
- 待处理:`txnStatus=dqr`
- 已处理:`txnStatus=yqr`
- [ ] **Step 4: 确认弹窗改造**
"确认收款"弹窗和"确认退款"弹窗合并为一个"确认操作"弹窗:
- 上半部分展示流水详情(渠道、金额/产品明细、提交人/时间/备注/凭证)
- 下半部分输入财务确认备注 + 上传确认凭证
- 调用 `fetchConfirmTxn(txnId, data)`
- [ ] **Step 5: 详情弹窗**
保留 SystemViewDialog 查看详情,展示流水完整信息。
---
## Task 8: 状态标签注册更新
**Files:**
- Modify: `web-admin/src/utils/status-tag-registry.ts`
- [ ] **Step 1: 新增流水类型标签**
在 `status-tag-registry.ts` 中新增:
```typescript
// 流水类型
export const txnTypeLabel = (type: string): string => {
const map: Record<string, string> = {
recharge: '充值',
refund: '退款',
purchase: '购买产品',
unsubscribe: '退订产品',
}
return map[type] || type
}
export const txnTypeTagType = (type: string): string => {
const map: Record<string, string> = {
recharge: '',
refund: 'danger',
purchase: 'success',
unsubscribe: 'warning',
}
return map[type] || 'info'
}
// 流水状态
export const txnStatusLabel = (status: string): string => {
const map: Record<string, string> = {
dsx: '待生效',
dqr: '待财务确认',
yqr: '已确认',
yzf: '已作废',
}
return map[status] || status
}
export const txnStatusTagType = (status: string): string => {
const map: Record<string, string> = {
dsx: 'info',
dqr: 'warning',
yqr: 'success',
yzf: 'danger',
}
return map[status] || 'info'
}
```
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,491 @@
# 渠道服务购买订单 — 功能设计文档
> 日期:2026-05-24
> 状态:待审阅
> 模块:中试服务管理
> 参考页面:`system/order/index.vue`(复杂列表)、`cell-storage/payment-confirm/index.vue`(财务确认)
---
## 1. 业务背景
渠道商(如医院、合作机构)通过签订渠道协议,以协议价格购买细胞产品服务。渠道服务购买订单支持两种模式:
1. **充值(预付款)**:渠道向账户充值金额,财务确认后余额增加。后续可用于购买产品。
2. **购买产品**:根据渠道协议价格购买细胞产品,直接从渠道账户余额扣减,同时增加产品份数库存。
后续中试服务单会从渠道产品份数库存中扣减。
---
## 2. 业务流程
### 2.1 充值订单 (order_type = yfk)
```
创建充值(待确认 dqr) → 财务确认(已确认 yqr, 余额+) → 发起退费(退费确认中 tfqrz) → 财务确认退费(已退费 ytf, 余额-)
```
| 状态 | 编码 | 操作角色 | 余额影响 |
|------|------|----------|----------|
| 待确认 | `dqr` | 采集人员创建 | 无 |
| 已确认 | `yqr` | 财务确认 | +prepay_amount |
| 退费确认中 | `tfqrz` | 采集人员发起退费 | 无 |
| 已退费 | `ytf` | 财务确认退费 | -refund_amount |
### 2.2 购买产品订单 (order_type = gmcp)
```
创建购买(生效中 sxz, 余额-, 份数+) → 作废(已作废 yzf, 余额+, 份数-)
```
| 状态 | 编码 | 操作角色 | 余额影响 | 份数影响 |
|------|------|----------|----------|----------|
| 生效中 | `sxz` | 采集人员创建时直接扣减 | -purchase_total_amount | +各产品 buy_qty |
| 已作废 | `yzf` | 采集人员/审核人员作废 | +purchase_total_amount | -各产品 buy_qty |
---
## 3. 数据库设计
### 3.1 现有表扩展 — `biz_channel_service_order`
现有 DDL(`docs/sql/zkxw.sql` 行 827-847)基础上新增以下字段:
```sql
ALTER TABLE biz_channel_service_order
ADD COLUMN channel_agreement_id VARCHAR(64) DEFAULT NULL COMMENT '渠道协议ID(购买订单必填,关联 biz_channel_agreement.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 '退费财务确认备注';
```
**设计决策:** 财务确认字段直接放主表(1:1 关系),不再使用现有空表 `biz_channel_service_order_finance_confirm`。余额变动历史通过 `balance_before / balance_after` 可追溯。
**完整字段清单(含原有 + 新增):**
| 字段 | 类型 | 说明 | 充值 | 购买 |
|------|------|------|------|------|
| id | VARCHAR(64) PK | 主键UUID | ✅ | ✅ |
| channel_id | VARCHAR(64) NOT NULL | 渠道ID | ✅ | ✅ |
| channel_name | VARCHAR(200) | 渠道名称冗余 | ✅ | ✅ |
| dept_name | VARCHAR(200) | 科室/部门 | ✅ | ✅ |
| order_type | VARCHAR(16) NOT NULL | yfk预付款 / gmcp购买产品 | ✅ | ✅ |
| prepay_amount | DECIMAL(18,2) | 预付金额 | ✅ | — |
| purchase_total_amount | DECIMAL(18,2) | 购买总金额 | — | ✅ |
| order_status | VARCHAR(16) NOT NULL DEFAULT 'sxz' | 订单状态 | dqr/yqr/tfqrz/ytf | sxz/yzf |
| channel_agreement_id | VARCHAR(64) | 渠道协议ID | — | ✅ 必填 |
| balance_before | DECIMAL(18,2) | 操作前余额 | 财务确认时填 | 创建时填 |
| balance_after | DECIMAL(18,2) | 操作后余额 | 财务确认时填 | 创建时填 |
| finance_confirm_time | DATETIME | 财务确认时间 | ✅ | — |
| finance_confirm_attachments | VARCHAR(4000) | 财务确认附件 | ✅ | — |
| finance_remark | VARCHAR(512) | 财务确认备注 | ✅ | — |
| refund_apply_by | VARCHAR(64) | 退费申请人 | ✅ | — |
| refund_apply_time | DATETIME | 退费申请时间 | ✅ | — |
| refund_amount | DECIMAL(18,2) | 退费金额 | ✅ | — |
| refund_remark | VARCHAR(512) | 退费备注 | ✅ | — |
| refund_attachments | VARCHAR(4000) | 退费附件 | ✅ | — |
| refund_finance_confirm_time | DATETIME | 退费确认时间 | ✅ | — |
| refund_finance_confirm_attachments | VARCHAR(4000) | 退费确认附件 | ✅ | — |
| refund_finance_remark | VARCHAR(512) | 退费确认备注 | ✅ | — |
### 3.2 现有表保持 — `biz_channel_service_order_product`
现有 DDL(行 888-909)无需改动。字段:`service_order_id`, `project_code`, `project_name`, `cell_type`, `spec_text`, `agreement_unit_price`, `buy_qty`, `buy_amount`。
### 3.3 新增表 — `biz_channel_account`(渠道账户余额)
```sql
CREATE TABLE 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='渠道账户余额';
```
**业务规则:**
- 一个渠道一条记录(UNIQUE KEY)
- `balance_amount` 实时更新,不可为负
- 余额变动历史通过 `biz_channel_service_order.balance_before / balance_after` 追溯
- 首次充值/购买时自动创建账户记录
### 3.4 新增表 — `biz_channel_product_quota`(渠道产品份数库存)
```sql
CREATE TABLE 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='渠道产品份数库存';
```
**业务规则:**
- 同一渠道同一产品规格只有一条库存记录(UNIQUE KEY)
- 购买订单创建时:`total_qty += buy_qty`, `remaining_qty += buy_qty`
- 购买订单作废时:`total_qty -= buy_qty`, `remaining_qty -= buy_qty`(需校验 remaining_qty >= buy_qty,防止已使用部分被退回)
- 后续中试服务单扣减时:`used_qty += qty`, `remaining_qty -= qty`(本次不实现,但预留字段)
### 3.5 现有空表 — `biz_channel_service_order_finance_confirm`
保留不删(当前无数据)。本次不使用,财务确认字段已放入主表。
### 3.6 表关系图
```
biz_channel (渠道)
├── biz_channel_agreement (协议)
│ └── biz_channel_agreement_product_price (协议产品价格)
├── biz_channel_account (账户余额) [NEW]
├── biz_channel_product_quota (产品份数库存) [NEW]
└── biz_channel_service_order (服务购买订单) [EXTEND]
├── biz_channel_service_order_product (产品明细)
└── (finance_confirm 字段已在主表)
```
---
## 4. 后端设计
### 4.1 SQLAlchemy 模型
在 `backend/app/models/models.py` 中新增:
1. **`BizChannelAccount`** — 对应 `biz_channel_account` 表
2. **`BizChannelProductQuota`** — 对应 `biz_channel_product_quota` 表
3. **`BizChannelServiceOrder`** — 扩展现有模型(如果已有则更新字段),对应 `biz_channel_service_order` + 新增字段
4. **`BizChannelServiceOrderProduct`** — 对应 `biz_channel_service_order_product`(如果已有则保持)
### 4.2 API 端点
在 `backend/app/api/pilot_service.py` 中扩展。前缀 `/web/admin/`。
#### 4.2.1 订单 CRUD
| 方法 | URL | 说明 | 权限 |
|------|-----|------|------|
| GET | `/channel-service-orders` | 订单列表(支持 order_type, order_status, channel_id, keyword 筛选) | 中试服务 |
| GET | `/channel-service-orders/{id}` | 订单详情(含产品明细列表) | 中试服务 |
| POST | `/channel-service-orders` | 创建订单 | 中试服务 |
| PUT | `/channel-service-orders/{id}` | 修改订单(仅 dqr/sxz 状态可改) | 中试服务 |
| DELETE | `/channel-service-orders/{id}` | 删除订单(仅 dqr 状态可删,软删除) | 中试服务 |
#### 4.2.2 状态流转操作
| 方法 | URL | 前置状态 | 目标状态 | 说明 | 操作角色 |
|------|-----|----------|----------|------|----------|
| POST | `/channel-service-orders/{id}/confirm-receipt` | dqr | yqr | 财务确认充值(余额+) | 财务 |
| POST | `/channel-service-orders/{id}/submit-refund` | yqr | tfqrz | 发起退费申请 | 中试服务 |
| POST | `/channel-service-orders/{id}/confirm-refund` | tfqrz | ytf | 财务确认退费(余额-) | 财务 |
| PUT | `/channel-service-orders/{id}/void` | sxz | yzf | 作废购买订单(余额+, 份数-) | 中试服务 |
#### 4.2.3 渠道账户 & 份数查询
| 方法 | URL | 说明 |
|------|-----|------|
| GET | `/channels/{channelId}/account` | 获取渠道账户余额(不存在则返回 balance=0) |
| GET | `/channels/{channelId}/product-quotas` | 获取渠道产品份数列表 |
### 4.3 CRUD 函数
在 `backend/app/crud/pilot_service.py` 中新增。遵循现有 `_row_to_dict_*` 模式。
**核心函数:**
| 函数名 | 说明 |
|--------|------|
| `_row_to_dict_channel_service_order` | 订单行转字典(单一真相源) |
| `list_channel_service_orders` | 列表查询(带筛选、分页、数据权限) |
| `get_channel_service_order` | 获取单条(含产品明细) |
| `create_channel_service_order` | 创建订单(充值直接创建;购买需扣余额+增份数) |
| `update_channel_service_order` | 修改订单(校验状态、业务字段) |
| `soft_delete_channel_service_order` | 软删除(仅 dqr 状态) |
| `confirm_receipt` | 财务确认充值(dqr→yqr,余额+) |
| `submit_refund` | 发起退费(yqr→tfqrz,记录退费信息) |
| `confirm_refund` | 财务确认退费(tfqrz→ytf,余额-) |
| `void_order` | 作废购买订单(sxz→yzf,余额+,份数-) |
| `_get_or_create_channel_account` | 获取或创建渠道账户 |
| `_upsert_channel_product_quota` | 新增或更新渠道产品份数 |
| `_reduce_channel_product_quota` | 扣减渠道产品份数(作废时用) |
### 4.4 核心业务逻辑
#### 创建充值订单
```
1. 校验 channel_id 存在
2. 校验 prepay_amount > 0
3. 创建订单 (order_type=yfk, order_status=dqr)
4. 不动余额
```
#### 创建购买订单
```
1. 校验 channel_id 存在
2. 校验 channel_agreement_id 存在且 agreement_status='sxz'
3. 从 biz_channel_agreement_product_price 加载协议价格
4. 校验所有产品明细的项目编码+细胞类型+规格在协议价格表中存在
5. 允许用户修改单价(agreement_unit_price 可与协议价不同)
6. 计算总金额 = sum(buy_qty * agreement_unit_price)
7. 获取渠道账户,校验 balance_amount >= purchase_total_amount
8. 扣减余额 (account.balance_amount -= total)
9. 记录 balance_before / balance_after
10. 创建订单 (order_type=gmcp, order_status=sxz) + 产品明细
11. 对每个产品明细,upsert biz_channel_product_quota (total_qty+=, remaining_qty+=)
```
#### 财务确认充值
```
1. 校验 order_status == 'dqr'
2. 获取/创建渠道账户
3. account.balance_amount += prepay_amount
4. 记录 balance_before, balance_after, finance_confirm_time, finance_confirm_attachments, finance_remark
5. order_status = 'yqr'
```
#### 发起退费
```
1. 校验 order_status == 'yqr'
2. 记录 refund_apply_by, refund_apply_time, refund_amount(=prepay_amount), refund_remark, refund_attachments
3. order_status = 'tfqrz'
```
#### 财务确认退费
```
1. 校验 order_status == 'tfqrz'
2. account.balance_amount -= refund_amount
3. 记录 balance_before, balance_after, refund_finance_confirm_time, refund_finance_confirm_attachments, refund_finance_remark
4. order_status = 'ytf'
```
#### 作废购买订单
```
1. 校验 order_status == 'sxz'
2. 查询产品明细列表
3. 对每个产品明细:
a. 校验 remaining_qty >= buy_qty(防止已使用部分被退回)
b. 如果 remaining_qty < buy_qty → 返回错误("产品 XXX 已使用 N 份,无法作废")
4. account.balance_amount += purchase_total_amount
5. 记录 balance_before, balance_after
6. 对每个产品明细:total_qty -= buy_qty, remaining_qty -= buy_qty
7. order_status = 'yzf'
```
---
## 5. 前端设计
### 5.1 菜单配置
在系统菜单管理中新增(中试服务管理 下):
| 菜单名称 | 路由路径 | 组件 | 说明 |
|----------|----------|------|------|
| 渠道服务购买订单 | `/pilot-service/channel-service-order` | `pilot-service/channel-service-order/index.vue` | 订单管理 |
| 渠道服务购买订单(财务确认) | `/pilot-service/channel-service-order-finance` | `pilot-service/channel-service-order-finance/index.vue` | 财务确认 |
### 5.2 订单管理页
**参考页面:** `system/order/index.vue`(复杂列表)
**搜索条件:**
- 订单类型(下拉:充值/购买产品)
- 订单状态(下拉:根据类型动态切换选项)
- 渠道名称(模糊搜索)
- 创建时间范围
**列表列:**
| 列 | 字段 | 宽度 | 说明 |
|----|------|------|------|
| 订单编号 | id 后 6 位 | min-width 120 | |
| 渠道名称 | channelName | min-width 160 | |
| 订单类型 | orderType | min-width 100 | Tag: yfk=充值(info), gmcp=购买产品(primary) |
| 订单金额 | prepayAmount / purchaseTotalAmount | min-width 120 | 根据 orderType 展示对应字段 |
| 订单状态 | orderStatus | min-width 100 | Tag,颜色见 5.5 |
| 操作前余额 | balanceBefore | min-width 120 | |
| 操作后余额 | balanceAfter | min-width 120 | |
| 创建时间 | createTime | min-width 160 | |
| 操作 | — | min-width 260, fixed="right" | 按状态显示按钮 |
**操作按钮规则:**
| 状态 | 可用操作 |
|------|----------|
| dqr(待确认) | 编辑、删除 |
| yqr(已确认) | 发起退费、查看 |
| tfqrz(退费确认中) | 查看 |
| ytf(已退费) | 查看 |
| sxz(生效中) | 查看、作废 |
| yzf(已作废) | 查看 |
**新增/编辑弹窗:**
- 选择渠道(下拉搜索,`fetchChannelList`)
- 订单类型(充值/购买产品,单选,新建后不可修改)
- **充值模式字段:** 预付金额(number input, > 0)
- **购买模式字段:**
- 选择渠道协议(下拉,仅显示该渠道 `agreement_status='sxz'` 的协议)
- 产品明细表格(动态行):
- 「添加产品」按钮 → 弹窗选择协议产品价格 → 填入行
- 每行:项目名称(只读)、细胞类型(只读)、规格(只读)、协议单价(可修改)、购买数量(必填)、购买金额(自动计算 = 单价 × 数量)
- 可删除行
- 底部汇总:合计金额 + 当前渠道余额(余额不足时红色提示)
- 备注(textarea)
**查看弹窗:**
- 展示订单基础信息 + 产品明细(只读表格)
- 充值订单:展示财务确认信息(确认时间、附件、备注)、退费信息(如有)
- 购买订单:展示余额变动信息
**作废确认弹窗:**
- 展示订单信息和产品明细
- 填写作废原因(textarea)
- 确认作废
**发起退费弹窗:**
- 展示充值金额
- 退费金额(默认=充值金额,可修改但不能超过充值金额)
- 退费备注(textarea)
- 退费附件(上传)
- 确认提交
### 5.3 财务确认页
**参考页面:** `cell-storage/payment-confirm/index.vue`
**Tab 1:待确认**
- 列表显示所有 `order_status IN ('dqr', 'tfqrz')` 的订单
- 操作:
- dqr 订单 → 「确认收款」按钮 → 弹窗(只读订单信息 + 财务备注 textarea + 附件上传)
- tfqrz 订单 → 「确认退费」按钮 → 弹窗(只读订单信息+退费信息 + 退费确认备注 + 退费确认附件)
**Tab 2:已确认**
- 列表显示所有 `order_status IN ('yqr', 'ytf')` 的订单
- 「查看详情」按钮 → 查看确认信息
### 5.4 渠道信息页扩展
在 `pilot-service/channel/index.vue` 的编辑弹窗或查看弹窗中新增「账户信息」Tab:
- 展示当前余额
- 展示产品份数库存列表(表格:项目名称、细胞类型、规格、总份数、已使用、剩余份数)
### 5.5 状态标签注册
在 `web-admin/src/utils/status-tag-registry.ts` 中新增:
```typescript
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' },
},
```
### 5.6 前端 API 封装
在 `web-admin/src/api/admin.ts` 中新增:
```typescript
// 渠道服务购买订单
fetchChannelServiceOrderList(params) // GET /channel-service-orders
fetchChannelServiceOrderDetail(id) // GET /channel-service-orders/{id}
fetchCreateChannelServiceOrder(data) // POST /channel-service-orders
fetchUpdateChannelServiceOrder(id, data) // PUT /channel-service-orders/{id}
fetchDeleteChannelServiceOrder(id) // DELETE /channel-service-orders/{id}
// 状态流转
fetchConfirmReceipt(id, data) // POST /channel-service-orders/{id}/confirm-receipt
fetchSubmitRefund(id, data) // POST /channel-service-orders/{id}/submit-refund
fetchConfirmRefund(id, data) // POST /channel-service-orders/{id}/confirm-refund
fetchVoidChannelServiceOrder(id, data) // PUT /channel-service-orders/{id}/void
// 渠道账户 & 份数
fetchChannelAccount(channelId) // GET /channels/{channelId}/account
fetchChannelProductQuotas(channelId) // GET /channels/{channelId}/product-quotas
```
---
## 6. 开发范围边界
### 本次实现 ✅
1. 数据库:2 张新表 + 1 张表扩展字段
2. 后端:4 个新 SQLAlchemy 模型 + CRUD 函数 + API 端点
3. 前端:2 个新页面(订单管理 + 财务确认)+ 渠道信息页扩展(账户 Tab)
4. 状态标签注册
5. 菜单数据初始化 SQL
### 后续实现 ❌(不在本次范围)
1. 中试服务单(biz_channel_pilot_service)的创建和产品份数扣减
2. 中试服务单与渠道购买订单的关联
3. 渠道账户对账报表
4. 渠道协议授信额度校验(可后续增强)
---
## 7. 注意事项
1. **并发安全:** 余额扣减和份数更新需使用数据库行锁(`SELECT ... FOR UPDATE`)或乐观锁,防止并发创建订单导致余额超扣。
2. **作废校验:** 购买订单作废时,需校验产品份数的 `remaining_qty >= buy_qty`。如果部分份数已被中试服务消耗(后续功能),则不允许作废或只能部分退回。
3. **数据权限:** 所有查询需应用 `apply_data_scope()` 数据范围过滤。
4. **租户隔离:** 所有操作带 `tenant_no` 过滤。
5. **操作日志:** 建议参考采集协议单的模式,记录订单状态变更日志(可选,后续增强)。
6. **前端参考页面:** 新页面严格遵循 `docs/web-admin-ui-spec-fresh-mint.md` 的 Fresh Mint 色彩规范。
7. **order_status 默认值:** 现有 DDL 中 `order_status DEFAULT 'sxz'`,但充值订单初始状态为 `dqr`。后端 create 函数必须根据 `order_type` 显式设置 order_status,不依赖 DDL 默认值:`order_type='yfk' → order_status='dqr'`,`order_type='gmcp' → order_status='sxz'`。
8. **cell_type 字段映射:** `biz_channel_service_order_product.cell_type` 存储细胞类型名称(冗余文本),但 `biz_channel_product_quota` 使用 `cell_type_code` + `cell_type_name` 分开存储。创建份数库存时,`cell_type_code` 和 `cell_type_name` 从 `biz_channel_agreement_product_price` 协议产品价格表获取,不从订单明细的 `cell_type` 取值。
9. **并发方案确定:** 余额扣减使用 `SELECT ... FOR UPDATE` 悲观行锁。在创建购买订单和财务确认充值时,先锁定 `biz_channel_account` 行再更新余额,防止并发超扣。整个余额操作在一个数据库事务内完成。
@@ -0,0 +1,256 @@
# 渠道服务订单 — 多次操作流水模型设计
## 背景
现有渠道服务订单(`biz_channel_service_order`)为单次操作模型:一个订单 = 一次充值或一次购买,退款只能做一次。业务要求每个订单支持多次充值、多次退款、多次购买产品、按份数退订产品,且所有涉及金额的操作都需走财务确认流程。
## 业务模型
```
渠道服务订单(运营手动创建,选渠道+协议,选类型 yfk/gmcp)
├── 充值型(yfk):独立充钱、独立退钱,走渠道账户余额
│ ├── 多次充值 → 每次独立金额 + 财务确认 → 余额增加
│ └── 多次退款 → 每次独立金额 + 财务确认 → 余额减少
│
└── 购买型(gmcp):独立买产品、独立退订,与余额无关
├── 多次购买 → 每次独立购买产品(份数) + 财务确认 → 持仓增加
└── 多次退订 → 按份数退订 + 财务确认 → 持仓减少
```
两种类型完全独立。充值管渠道余额,购买管产品份数。
## 方案:操作流水子表
核心思路:订单主表管汇总,新增操作流水表管明细,产品表管持仓。
---
## 1. 数据模型
### 1.1 订单主表 `biz_channel_service_order`(改造)
**保留字段:**
- `id`, `channel_id`, `channel_name`, `dept_name`, `order_type`(yfk/gmcp), `order_status`(dsx/sxz/yzf), `channel_agreement_id`
- 公共字段:`tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
**移除字段(迁移到流水表):**
- `prepay_amount`, `purchase_total_amount`
- `finance_submit_by`, `finance_submit_time`, `finance_submit_remark`, `finance_submit_attachments`
- `finance_confirm_time`, `finance_confirm_attachments`, `finance_remark`
- `refund_apply_by`, `refund_apply_time`, `refund_amount`, `refund_remark`, `refund_attachments`
- `refund_finance_confirm_time`, `refund_finance_confirm_attachments`, `refund_finance_remark`
- `balance_before`, `balance_after`
**新增汇总字段:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `total_recharge_amount` | decimal(18,2) DEFAULT 0 | 累计充值总额(yfk) |
| `total_refund_amount` | decimal(18,2) DEFAULT 0 | 累计退款总额(yfk) |
| `current_balance` | decimal(18,2) DEFAULT 0 | 当前余额(yfk),= 累计充值 - 累计退款 |
### 1.2 操作流水表 `biz_channel_service_order_txn`(新增)
```sql
CREATE TABLE `biz_channel_service_order_txn` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`service_order_id` varchar(64) NOT NULL COMMENT '关联订单ID',
`txn_type` varchar(16) NOT NULL COMMENT 'recharge/refund/purchase/unsubscribe',
`amount` decimal(18,2) DEFAULT NULL COMMENT '本次操作金额',
`txn_status` varchar(16) NOT NULL DEFAULT 'dsx' COMMENT 'dsx待生效 dqr待财务确认 yqr已确认 yzf已作废',
`products_json` text COMMENT '产品明细JSON(purchase/unsubscribe类型)',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
-- 发起财务确认
`finance_submit_by` varchar(64) DEFAULT NULL COMMENT '发起确认人ID',
`finance_submit_time` datetime DEFAULT NULL COMMENT '发起确认时间',
`finance_submit_remark` varchar(512) DEFAULT NULL COMMENT '发起确认备注',
`finance_submit_attachments` text COMMENT '发起确认附件',
-- 财务确认
`finance_confirm_time` datetime DEFAULT NULL COMMENT '财务确认时间',
`finance_confirm_attachments` text COMMENT '财务确认附件',
`finance_remark` varchar(512) DEFAULT NULL COMMENT '财务确认备注',
-- 余额快照(仅 yfk 类型)
`balance_before` decimal(18,2) DEFAULT NULL COMMENT '操作前余额',
`balance_after` decimal(18,2) DEFAULT NULL COMMENT '操作后余额',
-- 公共字段
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) 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 '删除标记',
PRIMARY KEY (`id`),
KEY `idx_txn_order` (`service_order_id`, `del_flag`),
KEY `idx_txn_status` (`txn_status`, `tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道服务订单操作流水';
```
### 1.3 产品持仓表 `biz_channel_service_order_product`(改造)
在现有字段基础上新增:
| 字段 | 类型 | 说明 |
|------|------|------|
| `total_buy_qty` | decimal(18,4) DEFAULT 0 | 累计购买数量 |
| `total_unsubscribe_qty` | decimal(18,4) DEFAULT 0 | 累计退订数量 |
| `current_qty` | decimal(18,4) DEFAULT 0 | 当前持有数量 |
| `total_unsubscribe_amount` | decimal(18,2) DEFAULT 0 | 累计退订金额 |
保留现有字段:`agreement_unit_price`, `buy_qty`(首次购买), `buy_amount`。
### 1.4 关系图
```
biz_channel_service_order(订单主表)
│
├── biz_channel_service_order_txn(操作流水)× N
│ 每条流水 = 一次充值/退款/购买/退订
│ 每条流水独立走财务确认流程
│
└── biz_channel_service_order_product(产品持仓)× N
购买时增加 current_qty,退订时减少
```
### 1.5 废弃表
`biz_channel_service_order_finance_confirm` — 已有但未使用,保留不删,不再使用。
---
## 2. 状态机
### 2.1 订单主表状态
| 状态 | 含义 | 说明 |
|------|------|------|
| `dsx` | 待生效 | 刚创建,尚无已确认的流水 |
| `sxz` | 生效中 | 至少有一笔流水已确认 |
| `yzf` | 已作废 | 手动作废,不再允许新操作 |
### 2.2 流水状态
| 状态 | 含义 | 可转至 |
|------|------|--------|
| `dsx` | 待生效 | `dqr`(发起确认)、`yzf`(作废) |
| `dqr` | 待财务确认 | `yqr`(财务确认) |
| `yqr` | 已确认(终态) | — |
| `yzf` | 已作废(终态) | — |
### 2.3 操作流转
**充值型(yfk):**
```
创建订单 → [dsx]
├─ 发起充值 → txn(recharge, dsx)
│ └─ 发起确认 → txn(dqr) → 财务确认 → txn(yqr)
│ └─ order.total_recharge_amount += amount
│ order.current_balance += amount
│ order.status → sxz (首次确认时)
│
└─ 申请退款 → txn(refund, dsx), amount ≤ current_balance
└─ 发起确认 → txn(dqr) → 财务确认 → txn(yqr)
└─ order.total_refund_amount += amount
order.current_balance -= amount
```
**购买型(gmcp):**
```
创建订单 → [dsx]
├─ 购买产品 → txn(purchase, dsx) + 创建/更新 product 持仓
│ └─ 发起确认 → txn(dqr) → 财务确认 → txn(yqr)
│ └─ product.total_buy_qty += qty
│ product.current_qty += qty
│ order.status → sxz (首次确认时)
│
└─ 退订产品 → txn(unsubscribe, dsx), qty ≤ product.current_qty
└─ 发起确认 → txn(dqr) → 财务确认 → txn(yqr)
└─ product.total_unsubscribe_qty += qty
product.current_qty -= qty
product.total_unsubscribe_amount += qty × unit_price
```
---
## 3. API 设计
### 3.1 订单管理(`/api/admin/channel-service-orders`)
| 接口 | 方法 | 说明 | 变更 |
|------|------|------|------|
| `/` | GET | 订单列表 | 不变 |
| `/{order_id}` | GET | 订单详情 + 流水列表 + 产品持仓 | **扩展返回** |
| `/` | POST | 创建订单 | 不变 |
| `/{order_id}` | PUT | 编辑订单基本信息 | 不变 |
| `/{order_id}` | DELETE | 删除 dsx 状态订单 | 不变 |
| `/{order_id}/void` | PUT | 作废订单 | 不变 |
| `/{order_id}/recharge` | POST | **新增** 发起充值流水 | 新增 |
| `/{order_id}/refund` | POST | **新增** 申请退款流水 | 替代原退款 |
| `/{order_id}/purchase` | POST | **新增** 购买产品流水 | 新增 |
| `/{order_id}/unsubscribe` | POST | **新增** 退订产品流水 | 新增 |
### 3.2 流水操作
| 接口 | 方法 | 说明 |
|------|------|------|
| `/channel-service-order-txns/{txn_id}/submit-finance` | POST | 发起财务确认 |
| `/channel-service-order-txns/{txn_id}/confirm` | POST | 财务确认(收款/退付) |
| `/channel-service-order-txns/{txn_id}/void` | PUT | 作废流水 |
### 3.3 财务确认页
| 接口 | 方法 | 说明 | 变更 |
|------|------|------|------|
| `/channel-service-order-txns` | GET | 查流水列表(按 status 筛选) | **改为查流水表** |
| `/channel-service-order-txns/{txn_id}` | GET | 流水详情 | 新增 |
| 确认 | POST | 复用 3.2 的 confirm 接口 | — |
---
## 4. 前端改造
### 4.1 订单管理页(`channel-service-order/index.vue`)
**操作按钮:**
| 条件 | 按钮 |
|------|------|
| yfk + dsx | 编辑、删除、发起充值 |
| yfk + sxz | 发起充值、申请退款 |
| gmcp + dsx | 编辑、删除、购买产品 |
| gmcp + sxz | 购买产品、退订产品 |
| 任意 + yzf | 仅查看 |
**详情弹窗改造:**
- Section 1:基本信息(含余额/汇总金额)
- Section 2:操作记录(流水时间线列表)
- Section 3:产品持仓(gmcp 类型,显示当前持有份数)
**新增弹窗:**
- 发起充值:金额、备注、凭证上传
- 申请退款:金额(≤余额)、备注、凭证上传
- 购买产品:选项目/规格、输入份数、自动算金额
- 退订产品:选已持有项目、输入退订份数(≤持有量)
### 4.2 财务确认页(`channel-service-order-finance/index.vue`)
**改为以流水为维度展示:**
**待处理 Tab(status=dqr):**
- 显示:流水类型标签、渠道名称、金额、产品明细、提交人/时间
- 操作:查看详情、确认收款/退付
**已处理 Tab(status=yqr):**
- 显示同上,操作仅查看
**确认弹窗:**
- 上半部分展示流水详情(发起确认信息)
- 下半部分输入财务确认备注+凭证
### 4.3 废弃功能
- 移除原订单级别的「发起财务确认」「申请退款」按钮和弹窗(改为流水级别操作)
- 移除订单主表上平铺的 finance/refund 字段展示