Kaynağa Gözat

feat: 员工额度发放记录/调整金额/变更记录(前后端)

alphah 2 hafta önce
ebeveyn
işleme
7d187578fd

+ 45 - 0
backend/app/plugin/module_payment/expense/quota/controller.py

@@ -10,6 +10,7 @@ from app.core.logger import log
 from app.core.router_class import OperationLogRoute
 
 from .schema import (
+    AdjustQuotaSchema,
     ExpenseQuotaCreateSchema,
     ExpenseQuotaDeleteSchema,
     ExpenseQuotaModifySchema,
@@ -222,3 +223,47 @@ async def outsource_notify_controller(
     result = await OutsourceNotifyService.notify_service(auth=auth, data=data)
     log.info(f"外部消费额度同步: out_source_id={result.out_source_id}, success={result.success}")
     return SuccessResponse(data=result, msg="外部消费额度同步成功" if result.success else "外部消费额度同步失败")
+
+
+@QuotaRouter.get(
+    "/employee/{employee_id}/records",
+    summary="查询员工额度发放记录",
+    description="查询该员工在指定/所有制度下的额度发放记录",
+)
+async def list_employee_quota_records_controller(
+    employee_id: Annotated[str, Path(description="员工ID")],
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:list"]))],
+    institution_id: Annotated[str | None, Query(description="制度ID")] = None,
+) -> JSONResponse:
+    items = await QuotaService.list_employee_quota_records_service(
+        auth=auth, employee_id=employee_id, institution_id=institution_id,
+    )
+    return SuccessResponse(data={"items": items, "total": len(items)}, msg="查询员工额度记录成功")
+
+
+@QuotaRouter.post(
+    "/{quota_id}/adjust",
+    summary="调整额度金额",
+    description="调整额度可用金额 (alipay.ebpp.invoice.expensecontrol.quota.modify)",
+)
+async def adjust_quota_controller(
+    quota_id: Annotated[str, Path(description="额度ID")],
+    data: AdjustQuotaSchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:update"]))],
+) -> JSONResponse:
+    result = await QuotaService.adjust_quota_service(auth=auth, data=data)
+    log.info(f"调整额度成功: quota_id={quota_id}, before={result['before_amount']}, after={result['after_amount']}")
+    return SuccessResponse(data=result, msg="调整额度成功")
+
+
+@QuotaRouter.get(
+    "/{quota_id}/changes",
+    summary="查询额度变更记录",
+    description="查询该额度的所有变更记录",
+)
+async def list_quota_changes_controller(
+    quota_id: Annotated[str, Path(description="额度ID")],
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:detail"]))],
+) -> JSONResponse:
+    items = await QuotaService.list_quota_changes_service(auth=auth, quota_id=quota_id)
+    return SuccessResponse(data={"items": items, "total": len(items)}, msg="查询变更记录成功")

+ 9 - 1
backend/app/plugin/module_payment/expense/quota/crud.py

@@ -3,7 +3,7 @@ from typing import Any
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.core.base_crud import CRUDBase
 
-from .model import IssueBatchModel, QuotaModel
+from .model import IssueBatchModel, QuotaChangeLogModel, QuotaModel
 from .schema import QuotaCreateSchema, QuotaUpdateSchema
 
 
@@ -41,3 +41,11 @@ class IssueBatchCRUD(CRUDBase[IssueBatchModel, dict[str, Any], dict[str, Any]]):
         self, issue_batch_id: str
     ) -> IssueBatchModel | None:
         return await self.get(issue_batch_id=issue_batch_id)
+
+
+class QuotaChangeLogCRUD(CRUDBase[QuotaChangeLogModel, dict[str, Any], dict[str, Any]]):
+    """额度变更记录 CRUD"""
+
+    def __init__(self, auth: AuthSchema) -> None:
+        self.auth = auth
+        super().__init__(model=QuotaChangeLogModel, auth=auth)

+ 36 - 0
backend/app/plugin/module_payment/expense/quota/model.py

@@ -100,3 +100,39 @@ class IssueBatchModel(PaymentModelMixin, TenantMixin, EnterpriseMixin):
     issue_desc: Mapped[str | None] = mapped_column(
         Text, comment="发放说明"
     )
+
+
+class QuotaChangeLogModel(PaymentModelMixin, TenantMixin, EnterpriseMixin):
+    """额度变更记录表"""
+
+    __tablename__ = "pay_expense_quota_change_log"
+    __table_args__ = {"comment": "额度变更记录表"}
+    __permission_strategy__ = PermissionFilterStrategy.ENTERPRISE_BASED
+
+    quota_id: Mapped[str] = mapped_column(
+        String(64), index=True, comment="额度ID"
+    )
+    employee_id: Mapped[str] = mapped_column(
+        String(64), comment="员工ID"
+    )
+    institution_id: Mapped[str] = mapped_column(
+        String(64), comment="制度ID"
+    )
+    change_type: Mapped[str] = mapped_column(
+        String(32), comment="变更类型: ISSUE(发放)/ADJUST(调整)/CANCEL(作废)"
+    )
+    coupon_name: Mapped[str | None] = mapped_column(
+        String(128), comment="点券名称"
+    )
+    change_amount: Mapped[Decimal] = mapped_column(
+        Numeric(12, 2), default=0, comment="变更金额(正为增加,负为减少)"
+    )
+    before_amount: Mapped[Decimal | None] = mapped_column(
+        Numeric(12, 2), comment="变更前金额"
+    )
+    after_amount: Mapped[Decimal | None] = mapped_column(
+        Numeric(12, 2), comment="变更后金额"
+    )
+    change_desc: Mapped[str | None] = mapped_column(
+        Text, comment="变更说明"
+    )

+ 53 - 0
backend/app/plugin/module_payment/expense/quota/schema.py

@@ -300,3 +300,56 @@ class IssueBatchListOutSchema(BaseModel):
     status: str = Field(description="状态")
     created_time: Optional[datetime] = Field(default=None, description="创建时间")
     updated_time: Optional[datetime] = Field(default=None, description="更新时间")
+
+
+# ========================
+# 员工额度记录 & 变更记录
+# ========================
+
+
+class EmployeeQuotaRecordItem(BaseModel):
+    """员工维度额度记录项"""
+
+    quota_id: Optional[str] = Field(default=None, description="额度ID")
+    out_biz_no: Optional[str] = Field(default=None, description="外部业务编号")
+    total_amount: Optional[float] = Field(default=None, description="发放金额")
+    available_amount: Optional[float] = Field(default=None, description="剩余可用金额")
+    quota_type: Optional[str] = Field(default=None, description="额度类型")
+    status: str = Field(description="状态")
+    valid_from: Optional[datetime] = Field(default=None, description="有效开始时间")
+    valid_to: Optional[datetime] = Field(default=None, description="有效结束时间")
+    created_time: Optional[datetime] = Field(default=None, description="发放时间")
+    institution_id: Optional[str] = Field(default=None, description="制度ID")
+
+
+class EmployeeQuotaRecordsOutSchema(BaseModel):
+    """员工额度记录列表响应"""
+
+    items: list[EmployeeQuotaRecordItem] = Field(default_factory=list, description="记录列表")
+    total: int = Field(default=0, description="总数")
+
+
+class AdjustQuotaSchema(BaseModel):
+    """调整额度请求"""
+
+    quota_id: str = Field(description="额度ID")
+    enterprise_id: str = Field(description="企业ID")
+    amount: float = Field(description="调整为的金额")
+    change_desc: Optional[str] = Field(default=None, description="调整说明")
+
+
+class QuotaChangeLogItem(BaseModel):
+    """变更记录项"""
+
+    coupon_name: Optional[str] = Field(default=None, description="点券名称")
+    change_time: Optional[datetime] = Field(default=None, description="变更时间")
+    change_amount: Optional[float] = Field(default=None, description="变更金额")
+    change_desc: Optional[str] = Field(default=None, description="变更说明")
+    change_type: Optional[str] = Field(default=None, description="变更类型")
+
+
+class QuotaChangeLogOutSchema(BaseModel):
+    """变更记录列表响应"""
+
+    items: list[QuotaChangeLogItem] = Field(default_factory=list, description="变更记录列表")
+    total: int = Field(default=0, description="总数")

+ 155 - 0
backend/app/plugin/module_payment/expense/quota/service.py

@@ -722,6 +722,161 @@ class QuotaService:
             out_schema=IssueBatchListOutSchema,
         )
 
+    @classmethod
+    async def list_employee_quota_records_service(
+        cls,
+        auth: AuthSchema,
+        employee_id: str,
+        institution_id: str | None = None,
+    ) -> list[dict]:
+        """查询员工的额度记录列表"""
+        from app.plugin.module_payment.expense.quota.model import QuotaModel
+        from sqlalchemy import select
+
+        where = [QuotaModel.employee_id == employee_id]
+        if institution_id:
+            where.append(QuotaModel.institution_id == institution_id)
+
+        stmt = select(QuotaModel).where(*where).order_by(QuotaModel.created_time.desc())
+        result = await auth.db.execute(stmt)
+        quotas = result.scalars().all()
+        return [
+            {
+                "quota_id": q.quota_id,
+                "out_biz_no": q.out_biz_no,
+                "total_amount": float(q.total_amount) if q.total_amount else 0,
+                "available_amount": float(q.available_amount) if q.available_amount else 0,
+                "quota_type": q.quota_type,
+                "status": q.status,
+                "valid_from": q.valid_from,
+                "valid_to": q.valid_to,
+                "created_time": q.created_time,
+                "institution_id": q.institution_id,
+            }
+            for q in quotas
+        ]
+
+    @classmethod
+    async def adjust_quota_service(
+        cls, auth: AuthSchema, data: AdjustQuotaSchema
+    ) -> dict:
+        """调整额度金额 (调Alipay quota.modify + 记录变更日志)"""
+        from .crud import QuotaChangeLogCRUD
+        from .model import QuotaModel
+        from sqlalchemy import select, update as sa_update
+
+        # 查询当前额度记录
+        stmt = select(QuotaModel).where(QuotaModel.quota_id == data.quota_id)
+        result = await auth.db.execute(stmt)
+        quota = result.scalar_one_or_none()
+        if not quota:
+            raise CustomException(msg="额度记录不存在")
+
+        current_available = float(quota.available_amount) if quota.available_amount else 0
+        diff = round(data.amount - current_available, 2)
+        outer_source_id = str(get_snowflake_id())
+
+        # 调Alipay quota.modify
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaModifyRequest import (
+                AlipayEbppInvoiceExpensecontrolQuotaModifyRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaModifyModel import (
+                AlipayEbppInvoiceExpensecontrolQuotaModifyModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaModifyResponse import (
+                AlipayEbppInvoiceExpensecontrolQuotaModifyResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装")
+
+        model = AlipayEbppInvoiceExpensecontrolQuotaModifyModel()
+        model.quota_id = data.quota_id
+        model.action = "ADD" if diff >= 0 else "DEDUCT"
+        model.outer_source_id = outer_source_id
+        model.enterprise_id = data.enterprise_id
+        model.amount = str(abs(diff))
+
+        request = AlipayEbppInvoiceExpensecontrolQuotaModifyRequest()
+        request.biz_model = model
+
+        client = AlipayClient.get_client()
+        response = client.execute(request)
+
+        if not response:
+            raise CustomException(msg="调整额度失败: 无响应")
+
+        mod_result = AlipayEbppInvoiceExpensecontrolQuotaModifyResponse()
+        mod_result.parse_response_content(response)
+
+        if not mod_result.is_success():
+            sub_msg = getattr(mod_result, 'sub_msg', '') or ''
+            log.error(f"支付宝接口调用失败: {mod_result.code} - {mod_result.msg}")
+            raise CustomException(msg=f"调整额度失败: {sub_msg or mod_result.msg}")
+
+        # 更新本地额度记录
+        new_available = data.amount
+        new_total = float(quota.total_amount) if quota.total_amount else 0
+        if diff > 0:
+            new_total += diff
+        else:
+            new_total = max(0, new_total + diff)
+
+        upd = sa_update(QuotaModel).where(
+            QuotaModel.quota_id == data.quota_id
+        ).values(
+            total_amount=new_total,
+            available_amount=new_available,
+        )
+        await auth.db.execute(upd)
+        await auth.db.flush()
+
+        # 记录变更日志
+        try:
+            log_crud = QuotaChangeLogCRUD(auth)
+            await log_crud.create({
+                "quota_id": data.quota_id,
+                "employee_id": quota.employee_id or "",
+                "institution_id": quota.institution_id or "",
+                "change_type": "ADJUST",
+                "coupon_name": quota.out_biz_no or "额度调整",
+                "change_amount": diff,
+                "before_amount": current_available,
+                "after_amount": new_available,
+                "change_desc": data.change_desc or "",
+                "enterprise_id": data.enterprise_id,
+                "tenant_id": auth.user.tenant_id if auth.user else 1,
+            })
+        except Exception as e:
+            log.warning(f"记录变更日志失败(不影响调整): {e}")
+
+        return {
+            "quota_id": data.quota_id,
+            "before_amount": current_available,
+            "after_amount": new_available,
+            "diff": diff,
+        }
+
+    @classmethod
+    async def list_quota_changes_service(
+        cls, auth: AuthSchema, quota_id: str
+    ) -> list[dict]:
+        """查询额度的变更记录"""
+        from .crud import QuotaChangeLogCRUD
+
+        crud = QuotaChangeLogCRUD(auth)
+        logs = await crud.list(search={"quota_id": quota_id}, order_by=[{"id": "desc"}])
+        return [
+            {
+                "coupon_name": log.coupon_name,
+                "change_time": log.created_time,
+                "change_amount": float(log.change_amount) if log.change_amount else 0,
+                "change_desc": log.change_desc,
+                "change_type": log.change_type,
+            }
+            for log in (logs or [])
+        ]
+
     @classmethod
     async def list_service(
         cls,

BIN
frontend/dist.zip


+ 66 - 1
frontend/src/api/module_payment/quota.ts

@@ -87,6 +87,34 @@ const QuotaAPI = {
       params: query,
     });
   },
+
+  // ========= 员工额度记录 & 调整 =========
+
+  /** 查询员工额度发放记录 */
+  employeeRecords(employeeId: string, params?: { institution_id?: string }) {
+    return request<ApiResponse<{ items: EmployeeQuotaRecord[]; total: number }>>({
+      url: `${API_PATH}/employee/${employeeId}/records`,
+      method: "get",
+      params,
+    });
+  },
+
+  /** 调整额度金额 */
+  adjustQuota(body: AdjustQuotaForm) {
+    return request<ApiResponse<AdjustQuotaResp>>({
+      url: `${API_PATH}/${body.quota_id}/adjust`,
+      method: "post",
+      data: body,
+    });
+  },
+
+  /** 查询额度变更记录 */
+  quotaChanges(quotaId: string) {
+    return request<ApiResponse<{ items: QuotaChangeLogItem[]; total: number }>>({
+      url: `${API_PATH}/${quotaId}/changes`,
+      method: "get",
+    });
+  },
 };
 
 export default QuotaAPI;
@@ -308,4 +336,41 @@ export const ISSUE_BATCH_STATUS_TAG: Record<string, string> = {
 export const ISSUE_BATCH_STATUS_LABEL: Record<string, string> = {
   ACTIVE: "有效",
   CANCELLED: "已作废",
-};
+};
+
+// ========= 员工额度记录 =========
+
+export interface EmployeeQuotaRecord {
+  quota_id?: string;
+  out_biz_no?: string;
+  total_amount?: number;
+  available_amount?: number;
+  quota_type?: string;
+  status: string;
+  valid_from?: string;
+  valid_to?: string;
+  created_time?: string;
+  institution_id?: string;
+}
+
+export interface AdjustQuotaForm {
+  quota_id: string;
+  enterprise_id: string;
+  amount: number;
+  change_desc?: string;
+}
+
+export interface AdjustQuotaResp {
+  quota_id?: string;
+  before_amount?: number;
+  after_amount?: number;
+  diff?: number;
+}
+
+export interface QuotaChangeLogItem {
+  coupon_name?: string;
+  change_time?: string;
+  change_amount?: number;
+  change_desc?: string;
+  change_type?: string;
+}

+ 84 - 0
frontend/src/views/module_payment/quota/components/AdjustAmountDialog.vue

@@ -0,0 +1,84 @@
+<template>
+  <el-dialog v-model="visible" title="调整额度" width="500px" destroy-on-close>
+    <el-form v-if="quotaData" label-width="120px">
+      <el-form-item label="额度ID">
+        <el-input :model-value="quotaData.quota_id" disabled />
+      </el-form-item>
+      <el-form-item label="当前可用额度">
+        <el-input :model-value="`¥${quotaData.available_amount?.toFixed(2) || '0.00'}`" disabled />
+      </el-form-item>
+      <el-form-item label="调整为" required>
+        <el-input-number v-model="formData.amount" :min="0" :precision="2" style="width: 200px" />
+      </el-form-item>
+      <el-form-item label="说明">
+        <el-input v-model="formData.change_desc" type="textarea" :rows="3" placeholder="可选" maxlength="200" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button type="primary" :loading="loading" @click="handleSubmit">确认调整</el-button>
+      <el-button @click="visible = false">取消</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, reactive } from "vue";
+import { ElMessage } from "element-plus";
+import QuotaAPI from "@/api/module_payment/quota";
+import { useEnterpriseStore } from "@/store/modules/enterprise.store";
+
+interface Props {
+  visible: boolean;
+  quotaData?: {
+    quota_id: string;
+    available_amount?: number;
+    total_amount?: number;
+  };
+}
+
+const props = withDefaults(defineProps<Props>(), { quotaData: () => ({ quota_id: "" }) });
+const emit = defineEmits<{
+  (e: "update:visible", v: boolean): void;
+  (e: "success"): void;
+}>();
+
+const visible = ref(false);
+const loading = ref(false);
+
+const formData = reactive({
+  amount: 0,
+  change_desc: "",
+});
+
+watch(() => props.visible, (v) => {
+  visible.value = v;
+  if (v && props.quotaData) {
+    formData.amount = props.quotaData.available_amount || 0;
+    formData.change_desc = "";
+  }
+});
+
+watch(visible, (v) => emit("update:visible", v));
+
+async function handleSubmit() {
+  const eid = useEnterpriseStore().getCurrentEnterprise?.enterprise_id;
+  if (!eid) { ElMessage.error("企业ID不存在"); return; }
+
+  loading.value = true;
+  try {
+    await QuotaAPI.adjustQuota({
+      quota_id: props.quotaData.quota_id,
+      enterprise_id: eid,
+      amount: formData.amount,
+      change_desc: formData.change_desc || undefined,
+    });
+    ElMessage.success("调整成功");
+    visible.value = false;
+    emit("success");
+  } catch (e: any) {
+    console.error("调整失败", e);
+  } finally {
+    loading.value = false;
+  }
+}
+</script>

+ 57 - 0
frontend/src/views/module_payment/quota/components/ChangeHistoryDialog.vue

@@ -0,0 +1,57 @@
+<template>
+  <el-dialog v-model="visible" title="变更记录" width="700px" destroy-on-close>
+    <el-table v-loading="loading" :data="list" border size="small" max-height="400">
+      <template #empty>
+        <el-empty :image-size="60" description="暂无变更记录" />
+      </template>
+      <el-table-column type="index" label="序号" width="50" />
+      <el-table-column prop="coupon_name" label="点券名称" min-width="120" show-overflow-tooltip />
+      <el-table-column prop="change_time" label="变更时间" width="160" />
+      <el-table-column prop="change_amount" label="变更金额" width="100" align="right">
+        <template #default="scope">
+          <span :style="{ color: scope.row.change_amount >= 0 ? '#67c23a' : '#f56c6c' }">
+            {{ scope.row.change_amount >= 0 ? "+" : "" }}{{ scope.row.change_amount?.toFixed(2) }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="change_desc" label="变更说明" min-width="160" show-overflow-tooltip />
+    </el-table>
+    <template #footer>
+      <el-button @click="visible = false">关闭</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from "vue";
+import QuotaAPI from "@/api/module_payment/quota";
+
+interface Props {
+  visible: boolean;
+  quotaId: string;
+}
+
+const props = defineProps<Props>();
+const emit = defineEmits<{ (e: "update:visible", v: boolean): void }>();
+
+const visible = ref(false);
+const loading = ref(false);
+const list = ref<any[]>([]);
+
+watch(() => props.visible, async (v) => {
+  visible.value = v;
+  if (v && props.quotaId) {
+    loading.value = true;
+    try {
+      const res = await QuotaAPI.quotaChanges(props.quotaId);
+      list.value = res.data.data?.items || [];
+    } catch (e) {
+      console.error("加载变更记录失败", e);
+    } finally {
+      loading.value = false;
+    }
+  }
+});
+
+watch(visible, (v) => emit("update:visible", v));
+</script>

+ 164 - 0
frontend/src/views/module_payment/quota/components/EmployeeQuotaDetailDialog.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-dialog v-model="visible" title="员工额度发放记录" width="1000px" destroy-on-close>
+    <div v-if="employeeInfo" style="margin-bottom: 16px;">
+      <el-descriptions :column="4" border size="small">
+        <el-descriptions-item label="员工姓名">{{ employeeInfo.name || "-" }}</el-descriptions-item>
+        <el-descriptions-item label="工号">{{ employeeInfo.employee_no || "-" }}</el-descriptions-item>
+        <el-descriptions-item label="部门">{{ employeeInfo.department_name || "-" }}</el-descriptions-item>
+        <el-descriptions-item label="手机号">{{ employeeInfo.mobile || "-" }}</el-descriptions-item>
+      </el-descriptions>
+    </div>
+
+    <el-table v-loading="loading" :data="records" border size="small" max-height="450">
+      <template #empty>
+        <el-empty :image-size="60" description="暂无额度记录" />
+      </template>
+      <el-table-column type="index" label="序号" width="50" />
+      <el-table-column prop="created_time" label="发放时间" width="150" />
+      <el-table-column prop="coupon_name" label="发放名称" min-width="120" show-overflow-tooltip />
+      <el-table-column label="有效期" min-width="200">
+        <template #default="scope">
+          {{ scope.row.valid_from ? formatTime(scope.row.valid_from) : "-" }}
+          ~
+          {{ scope.row.valid_to ? formatTime(scope.row.valid_to) : "-" }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="status" label="状态" width="70" align="center">
+        <template #default="scope">
+          <el-tag :type="STATUS_TAG_TYPE[scope.row.status] || 'info'" size="small">
+            {{ STATUS_LABEL[scope.row.status] || scope.row.status }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="total_amount" label="发放金额" width="100" align="right">
+        <template #default="scope">
+          ¥{{ scope.row.total_amount?.toFixed(2) || "0.00" }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="available_amount" label="剩余可用" width="100" align="right">
+        <template #default="scope">
+          ¥{{ scope.row.available_amount?.toFixed(2) || "0.00" }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="160" align="center" fixed="right">
+        <template #default="scope">
+          <el-button type="primary" size="small" link @click="handleAdjust(scope.row)">
+            调整金额
+          </el-button>
+          <el-button type="info" size="small" link @click="handleChanges(scope.row)">
+            变更记录
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <template #footer>
+      <el-button @click="visible = false">关闭</el-button>
+    </template>
+
+    <AdjustAmountDialog
+      v-model:visible="adjustVisible"
+      :quota-data="currentQuotaData"
+      @success="loadRecords"
+    />
+    <ChangeHistoryDialog
+      v-model:visible="changesVisible"
+      :quota-id="currentQuotaId"
+    />
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from "vue";
+import QuotaAPI, { STATUS_TAG_TYPE, STATUS_LABEL } from "@/api/module_payment/quota";
+import EmployeeAPI from "@/api/module_payment/employee";
+import AdjustAmountDialog from "./AdjustAmountDialog.vue";
+import ChangeHistoryDialog from "./ChangeHistoryDialog.vue";
+
+interface Props {
+  visible: boolean;
+  employeeId: string;
+  institutionId?: string;
+  employeeName?: string;
+}
+
+const props = defineProps<Props>();
+const emit = defineEmits<{ (e: "update:visible", v: boolean): void }>();
+
+const visible = ref(false);
+const loading = ref(false);
+const records = ref<any[]>([]);
+const employeeInfo = ref<any>(null);
+const adjustVisible = ref(false);
+const changesVisible = ref(false);
+const currentQuotaData = ref<any>({});
+const currentQuotaId = ref("");
+
+watch(() => props.visible, async (v) => {
+  visible.value = v;
+  if (v && props.employeeId) {
+    await Promise.all([loadEmployee(), loadRecords()]);
+  }
+});
+
+watch(visible, (v) => emit("update:visible", v));
+
+async function loadEmployee() {
+  const eid = "";
+  try {
+    const res = await EmployeeAPI.detailEmployee({ employee_id: props.employeeId } as any);
+    const emp = res?.data?.data;
+    employeeInfo.value = {
+      name: emp?.employee_name || props.employeeName || "-",
+      employee_no: emp?.employee_no || "-",
+      department_name: "",
+      mobile: emp?.employee_mobile || "-",
+    };
+  } catch (e) {
+    employeeInfo.value = { name: props.employeeName || props.employeeId, employee_no: "-", department_name: "-", mobile: "-" };
+  }
+}
+
+function getBatchNo(outBizNo?: string): string {
+  if (!outBizNo) return "-";
+  const parts = outBizNo.split("_");
+  return parts.length >= 2 ? parts[1] : outBizNo;
+}
+
+async function loadRecords() {
+  loading.value = true;
+  try {
+    const res = await QuotaAPI.employeeRecords(props.employeeId, {
+      institution_id: props.institutionId || undefined,
+    });
+    const items = res.data.data?.items || [];
+    records.value = items.map((r: any) => ({
+      ...r,
+      coupon_name: getBatchNo(r.out_biz_no),
+    }));
+  } catch (e) {
+    console.error("加载记录失败", e);
+  } finally {
+    loading.value = false;
+  }
+}
+
+function formatTime(t?: string) {
+  if (!t) return "-";
+  return t.substring(0, 16);
+}
+
+function handleAdjust(row: any) {
+  currentQuotaData.value = {
+    quota_id: row.quota_id,
+    available_amount: row.available_amount,
+    total_amount: row.total_amount,
+  };
+  adjustVisible.value = true;
+}
+
+function handleChanges(row: any) {
+  currentQuotaId.value = row.quota_id;
+  changesVisible.value = true;
+}
+</script>

+ 106 - 0
frontend/src/views/module_payment/quota/index.vue

@@ -263,6 +263,62 @@
       </template>
     </el-dialog>
 
+    <!-- 员工发放记录 -->
+    <template v-if="activeCategory === 'employee'">
+      <div class="employee-records-section">
+        <div style="margin-bottom: 16px;">
+          <el-form :inline="true">
+            <el-form-item label="员工ID">
+              <el-input v-model="employeeIdSearch" placeholder="请输入员工ID" clearable style="width: 200px" />
+            </el-form-item>
+            <el-form-item label="制度ID">
+              <el-input v-model="employeeInstSearch" placeholder="可选" clearable style="width: 200px" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="handleSearchEmployee">查询</el-button>
+              <el-button @click="employeeIdSearch = ''; employeeInstSearch = ''; employeesData = []; empLoading = false">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+
+        <el-table v-loading="empLoading" :data="employeesData" border max-height="calc(100vh - 300px)">
+          <template #empty>
+            <el-empty :image-size="60" description="请输入员工ID查询" />
+          </template>
+          <el-table-column type="index" label="序号" width="50" />
+          <el-table-column prop="employee_id" label="员工ID" min-width="160" show-overflow-tooltip />
+          <el-table-column label="员工姓名" min-width="120">
+            <template #default="scope">
+              {{ scope.row?.name || "-" }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="employee_no" label="工号" width="120" />
+          <el-table-column prop="department_name" label="部门" width="120" />
+          <el-table-column prop="employee_mobile" label="手机号" width="130" />
+          <el-table-column label="已发点券" width="100" align="right">
+            <template #default="scope">
+              {{ scope.row.couponCount ?? "-" }}
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="100" align="center">
+            <template #default="scope">
+              <el-button type="primary" size="small" link @click="handleViewEmployeeRecords(scope.row)">
+                详情
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </template>
+
+    <!-- 员工额度记录弹窗 -->
+    <EmployeeQuotaDetailDialog
+      v-model:visible="empRecordsDialogVisible"
+      :employee-id="currentEmpId"
+      :institution-id="employeeInstSearch || undefined"
+      :employee-name="currentEmpName"
+    />
+
     <!-- 发放记录弹窗 -->
     <el-dialog v-model="recordsDialogVisible" title="发放明细" width="900px" destroy-on-close>
       <IssueBatchDetail
@@ -301,11 +357,13 @@ import QuotaForm from "./components/QuotaForm.vue";
 import QuotaDetail from "./components/QuotaDetail.vue";
 import IssueBatchForm from "./components/IssueBatchForm.vue";
 import IssueBatchDetail from "./components/IssueBatchDetail.vue";
+import EmployeeQuotaDetailDialog from "./components/EmployeeQuotaDetailDialog.vue";
 import type { ISearchConfig, IContentConfig } from "@/components/CURD/types";
 import { useCrudList } from "@/components/CURD/useCrudList";
 import { useLoadingAction } from "@/composables/useLoadingAction";
 import { useRoute } from "vue-router";
 import { ElMessage, ElMessageBox } from "element-plus";
+import EmployeeAPI from "@/api/module_payment/employee";
 import { ref, reactive, computed, onMounted } from "vue";
 import { useEnterpriseStore } from "@/store/modules/enterprise.store";
 
@@ -324,9 +382,57 @@ const enterpriseIdFromUrl = computed(() => route.query.enterprise_id as string |
 const categoryTabs = [
   { key: "quota", label: "额度管理" },
   { key: "batch", label: "手工发放" },
+  { key: "employee", label: "员工发放记录" },
 ];
 const activeCategory = ref((route.query.tab as string) || "quota");
 
+// ===== 员工发放记录 =====
+const employeeIdSearch = ref("");
+const employeeInstSearch = ref("");
+const employeesData = ref<any[]>([]);
+const empLoading = ref(false);
+const empRecordsDialogVisible = ref(false);
+const currentEmpId = ref("");
+const currentEmpName = ref("");
+
+async function handleSearchEmployee() {
+  if (!employeeIdSearch.value) return;
+  empLoading.value = true;
+  try {
+    const res = await EmployeeAPI.detailEmployee({ employee_id: employeeIdSearch.value } as any);
+    const emp = res?.data?.data;
+    if (emp) {
+      // 查询该员工的额度记录总数
+      const quotaRes = await QuotaAPI.employeeRecords(employeeIdSearch.value, {
+        institution_id: employeeInstSearch.value || undefined,
+      });
+      const records = quotaRes.data.data?.items || [];
+      employeesData.value = [{
+        employee_id: emp.employee_id,
+        name: emp.employee_name,
+        employee_no: emp.employee_no,
+        department_name: "",
+        employee_mobile: emp.employee_mobile,
+        couponCount: records.length,
+      }];
+    } else {
+      employeesData.value = [];
+      ElMessage.warning("未找到该员工");
+    }
+  } catch (e) {
+    console.error("查询员工失败", e);
+    employeesData.value = [];
+  } finally {
+    empLoading.value = false;
+  }
+}
+
+function handleViewEmployeeRecords(row: any) {
+  currentEmpId.value = row.employee_id;
+  currentEmpName.value = row.name || "";
+  empRecordsDialogVisible.value = true;
+}
+
 onMounted(() => {
   if (activeCategory.value === "batch") loadBatchList();
 });