Files
junhong_cmp_fiber/internal/handler/admin/employee_collection.go
break ce24d5612e
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
feat(员工代收款): 新增员工代收款账单闭环
- 新增 6 张表与成对迁移 000212,扩展企业微信审批场景业务类型白名单
- 后台线下套餐订单与两条代理线下充值入账路径在来源成功事务内建账,来源唯一键幂等
- 核销申请、审批尝试记录、账单分摊预占与驳回重提,审批业务类型 employee_collection_approval
- 企业微信终态消费幂等:通过转已核销、驳回释放预占、通过后撤销不回滚并转异常终态
- 退款成功事务内按 bill_id+refund_id 幂等冲销账单或仅写退款关联提示
- 线下收款方式字典、账单查询/统计/关闭、申请查询与代办权限,均写入事务内审计

OpenSpec Change: add-employee-collection-bills
2026-09-10 18:24:05 +08:00

273 lines
10 KiB
Go

package admin
import (
"strings"
"time"
"github.com/gofiber/fiber/v2"
employeecollectionapp "github.com/break/junhong_cmp_fiber/internal/application/employeecollection"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
employeecollectionquery "github.com/break/junhong_cmp_fiber/internal/query/employeecollection"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// EmployeeCollectionHandler 处理员工代收款账单、核销申请与线下收款方式字典请求。
// 边界只做绑定、路径参数校验与统一响应,状态、金额与权限不变量由应用用例判断。
type EmployeeCollectionHandler struct {
paymentMethod *employeecollectionapp.PaymentMethodService
paymentMethodQuery *employeecollectionquery.PaymentMethodQuery
billClose *employeecollectionapp.BillCloseService
billQuery *employeecollectionquery.BillQuery
application *employeecollectionapp.ApplicationService
applicationQuery *employeecollectionquery.ApplicationQuery
}
// NewEmployeeCollectionHandler 创建员工代收款处理器。
func NewEmployeeCollectionHandler(
service *employeecollectionapp.PaymentMethodService,
billClose *employeecollectionapp.BillCloseService,
application *employeecollectionapp.ApplicationService,
) *EmployeeCollectionHandler {
return &EmployeeCollectionHandler{paymentMethod: service, billClose: billClose, application: application}
}
// SetApplicationQuery 注入核销申请只读投影。
func (h *EmployeeCollectionHandler) SetApplicationQuery(query *employeecollectionquery.ApplicationQuery) {
h.applicationQuery = query
}
// SetPaymentMethodQuery 注入线下收款方式字典只读投影。
func (h *EmployeeCollectionHandler) SetPaymentMethodQuery(query *employeecollectionquery.PaymentMethodQuery) {
h.paymentMethodQuery = query
}
// SetBillQuery 注入员工代收款账单只读投影。
func (h *EmployeeCollectionHandler) SetBillQuery(query *employeecollectionquery.BillQuery) {
h.billQuery = query
}
// CreatePaymentMethod 创建线下收款方式。
// POST /api/admin/employee-collection-payment-methods
func (h *EmployeeCollectionHandler) CreatePaymentMethod(c *fiber.Ctx) error {
var request dto.CreateEmployeeCollectionPaymentMethodRequest
if err := c.BodyParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.paymentMethod.Create(c.UserContext(), request)
if err != nil {
return err
}
return response.Success(c, result)
}
// UpdatePaymentMethod 更新线下收款方式,支持修改名称、排序、启停、备注与未被引用时的稳定编码。
// PUT /api/admin/employee-collection-payment-methods/:id
func (h *EmployeeCollectionHandler) UpdatePaymentMethod(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
var request dto.UpdateEmployeeCollectionPaymentMethodRequest
if err := c.BodyParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.paymentMethod.Update(c.UserContext(), id, request)
if err != nil {
return err
}
return response.Success(c, result)
}
// DeletePaymentMethod 删除未被核销申请引用的线下收款方式。
// DELETE /api/admin/employee-collection-payment-methods/:id
func (h *EmployeeCollectionHandler) DeletePaymentMethod(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
if err := h.paymentMethod.Delete(c.UserContext(), id); err != nil {
return err
}
return response.Success(c, nil)
}
// ListPaymentMethods 分页查询线下收款方式。
// GET /api/admin/employee-collection-payment-methods
func (h *EmployeeCollectionHandler) ListPaymentMethods(c *fiber.Ctx) error {
var request dto.EmployeeCollectionPaymentMethodListRequest
if err := c.QueryParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.paymentMethodQuery.List(c.UserContext(), request)
if err != nil {
return err
}
return response.SuccessWithPagination(c, result.List, result.Total, result.Page, result.PageSize)
}
// ListBills 分页查询员工代收款账单。
// GET /api/admin/employee-collection-bills
func (h *EmployeeCollectionHandler) ListBills(c *fiber.Ctx) error {
var request dto.EmployeeCollectionBillListRequest
if err := c.QueryParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.billQuery.List(c.UserContext(), request)
if err != nil {
return err
}
return response.SuccessWithPagination(c, result.List, result.Total, result.Page, result.PageSize)
}
// StatisticsBills 汇总员工代收款账单金额与待处理数量。
// GET /api/admin/employee-collection-bills/statistics
func (h *EmployeeCollectionHandler) StatisticsBills(c *fiber.Ctx) error {
var request dto.EmployeeCollectionBillStatisticsRequest
if err := c.QueryParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.billQuery.Statistics(c.UserContext(), request)
if err != nil {
return err
}
return response.Success(c, result)
}
// GetBill 查询员工代收款账单详情。
// GET /api/admin/employee-collection-bills/:id
func (h *EmployeeCollectionHandler) GetBill(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
result, err := h.billQuery.Detail(c.UserContext(), id)
if err != nil {
return err
}
return response.Success(c, result)
}
// CloseBill 关闭员工代收款账单,仅超级管理员可操作。
// POST /api/admin/employee-collection-bills/:id/close
func (h *EmployeeCollectionHandler) CloseBill(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
var request dto.CloseEmployeeCollectionBillRequest
if err := c.BodyParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
bill, err := h.billClose.Close(c.UserContext(), id, request.Reason)
if err != nil {
return err
}
result, err := employeecollectionquery.ProjectBill(bill)
if err != nil {
return err
}
return response.Success(c, result)
}
// CreateApplication 为本人可见账单创建核销申请。
// POST /api/admin/employee-collection-applications
func (h *EmployeeCollectionHandler) CreateApplication(c *fiber.Ctx) error {
request, err := bindApplicationRequest(c)
if err != nil {
return err
}
result, err := h.application.Create(c.UserContext(), request)
if err != nil {
return err
}
projected, err := employeecollectionquery.ProjectApplicationSubmit(employeecollectionquery.ApplicationSubmitProjection{
Application: result.Application, Attempt: result.Attempt, Allocations: result.Allocations,
Bills: result.Bills, InstanceID: result.InstanceID, InstanceStatus: result.InstanceStatus,
})
if err != nil {
return err
}
return response.Success(c, projected)
}
// ResubmitApplication 修改并重提已驳回的核销申请。
// PUT /api/admin/employee-collection-applications/:id
func (h *EmployeeCollectionHandler) ResubmitApplication(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
request, err := bindApplicationRequest(c)
if err != nil {
return err
}
result, err := h.application.Resubmit(c.UserContext(), id, request)
if err != nil {
return err
}
projected, err := employeecollectionquery.ProjectApplicationSubmit(employeecollectionquery.ApplicationSubmitProjection{
Application: result.Application, Attempt: result.Attempt, Allocations: result.Allocations,
Bills: result.Bills, InstanceID: result.InstanceID, InstanceStatus: result.InstanceStatus,
})
if err != nil {
return err
}
return response.Success(c, projected)
}
// ListApplications 分页查询核销申请。
// GET /api/admin/employee-collection-applications
func (h *EmployeeCollectionHandler) ListApplications(c *fiber.Ctx) error {
var request dto.EmployeeCollectionApplicationListRequest
if err := c.QueryParser(&request); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.applicationQuery.List(c.UserContext(), request)
if err != nil {
return err
}
return response.SuccessWithPagination(c, result.List, result.Total, result.Page, result.PageSize)
}
// GetApplication 查询核销申请详情,含分摊与全部审批尝试历史。
// GET /api/admin/employee-collection-applications/:id
func (h *EmployeeCollectionHandler) GetApplication(c *fiber.Ctx) error {
id, err := pathID(c)
if err != nil {
return err
}
result, err := h.applicationQuery.Detail(c.UserContext(), id)
if err != nil {
return err
}
return response.Success(c, result)
}
// bindApplicationRequest 绑定并转换创建或重提核销申请请求。
// 付款时间按带时区的 RFC3339 解析;其余边界与业务校验由应用用例统一判断。
func bindApplicationRequest(c *fiber.Ctx) (employeecollectionapp.SubmitApplicationCommand, error) {
var request dto.SubmitEmployeeCollectionApplicationRequest
if err := c.BodyParser(&request); err != nil {
return employeecollectionapp.SubmitApplicationCommand{}, errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
paidAt, err := time.Parse(time.RFC3339, strings.TrimSpace(request.PaidAt))
if err != nil {
return employeecollectionapp.SubmitApplicationCommand{}, errors.New(errors.CodeInvalidParam, "付款时间必须为带时区的 RFC3339 格式")
}
allocations := make([]employeecollectionapp.ApplicationAllocationCommand, 0, len(request.Allocations))
for _, item := range request.Allocations {
allocations = append(allocations, employeecollectionapp.ApplicationAllocationCommand{
BillID: item.BillID, Amount: item.Amount,
})
}
return employeecollectionapp.SubmitApplicationCommand{
PaymentMethodID: request.PaymentMethodID, PaidAmount: request.PaidAmount,
PayerName: request.PayerName, PaidAt: paidAt,
ExternalTransactionNo: request.ExternalTransactionNo,
PaymentVoucherKeys: request.PaymentVoucherKeys, Remark: request.Remark,
ActingReason: request.ActingReason, Allocations: allocations,
}, nil
}