Files
platforms/backend/api/internal/logic/delivery/order.go

542 lines
19 KiB
Go
Raw Normal View History

2026-08-23 01:08:25 +08:00
// 功能描述:实现配送点范围内的合同、合同气瓶和配送订单接口。
2026-08-23 11:30:52 +08:00
// 版本v1.2.0。
package delivery
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math"
2026-08-23 01:08:25 +08:00
"strings"
"time"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
platformgasorder "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/gasorder"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func rewriteJSON(ctx *gin.Context, mutate func(map[string]any) bool) bool {
body, err := io.ReadAll(ctx.Request.Body)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return false
}
var values map[string]any
if json.Unmarshal(body, &values) != nil || !mutate(values) {
if !ctx.IsAborted() {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
}
return false
}
body, _ = json.Marshal(values)
ctx.Request.Body = io.NopCloser(bytes.NewReader(body))
return true
}
func scopedContract(ctx *gin.Context, identity string, pointID uint64) (models.GasorderContract, bool) {
var contract models.GasorderContract
if err := common.ActiveRecords(db()).Where("identity = ? AND delivery_basic_id = ?", identity, pointID).First(&contract).Error; err != nil {
common.RespondRecordError(ctx, err)
return contract, false
}
return contract, true
}
2026-08-23 11:30:52 +08:00
// deliveryContractListQuery 构造当前配送点合同列表;订单候选模式额外限定可履约状态。
func deliveryContractListQuery(database *gorm.DB, gasID, pointID uint64, candidate string, now time.Time) *gorm.DB {
query := platformgasorder.ContractPartyDisplayQuery(database).
Where("gasorder_contract.gas_basic_id = ? AND gasorder_contract.delivery_basic_id = ?", gasID, pointID)
if candidate == "order" {
return platformgasorder.FilterGasorderContractCandidates(query, "order", now)
}
return query
}
func ListContract(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
2026-08-23 01:08:25 +08:00
if !ok {
return
}
page, size := common.PageSize(ctx)
2026-08-23 11:30:52 +08:00
query := deliveryContractListQuery(
db(), point.GasBasicID, point.ID, strings.TrimSpace(ctx.Query("candidate")), time.Now(),
)
query = common.ApplyKeywordFilter(ctx, query, &models.GasorderContract{})
2026-08-23 01:08:25 +08:00
var total int64
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
2026-08-23 11:30:52 +08:00
var list []platformgasorder.ContractPartyDisplay
if err := query.Order("gasorder_contract.created_at desc").Offset((page - 1) * size).Limit(size).Scan(&list).Error; err != nil {
2026-08-23 01:08:25 +08:00
infra.Response.Error(ctx, err)
return
}
response, err := common.PublicResourceResponse(list)
if err != nil {
infra.Response.Error(ctx, err)
return
}
2026-08-23 11:30:52 +08:00
contracts := make([]models.GasorderContract, 0, len(list))
for _, item := range list {
contracts = append(contracts, item.GasorderContract)
}
protected, err := protectDeliveryContractListResponse(ctx, response, contracts)
2026-08-23 01:08:25 +08:00
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"total": total, "list": protected})
}
// protectDeliveryContractListResponse 删除内部附件路径,仅补充无敏感信息的存在性标识。
func protectDeliveryContractListResponse(ctx *gin.Context, response any, list []models.GasorderContract) ([]any, error) {
protected := common.ProtectPreciseLocation(ctx, &models.GasorderContract{}, response)
items, valid := protected.([]any)
if !valid || len(items) != len(list) {
return nil, errors.New("配送合同列表响应结构无效")
}
for index, item := range items {
row, rowValid := item.(map[string]any)
if !rowValid {
return nil, errors.New("配送合同列表记录结构无效")
}
// 只返回是否存在附件,内部存储 URI 已由统一保护层删除。
row["has_attachment"] = strings.TrimSpace(list[index].FileURI) != ""
}
return items, nil
}
func GetContract(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if ok {
if _, valid := scopedContract(ctx, ctx.Param("identity"), point.ID); valid {
platformgasorder.GetGasorderContract(ctx)
}
}
}
2026-08-23 01:08:25 +08:00
// UploadContractAttachment 在当前配送点登录范围内上传受控 PDF 临时附件。
func UploadContractAttachment(ctx *gin.Context) {
if _, _, ok := currentScope(ctx); !ok {
return
}
platformgasorder.UploadGasorderContractAttachment(ctx)
}
// CleanupContractAttachment 清理当前配送点操作人尚未绑定的临时附件。
func CleanupContractAttachment(ctx *gin.Context) {
if _, _, ok := currentScope(ctx); !ok {
return
}
platformgasorder.CleanupGasorderContractAttachment(ctx)
}
// ServeContractAttachment 仅预览当前配送点拥有的正式合同附件。
func ServeContractAttachment(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
if _, valid := scopedContract(ctx, ctx.Param("identity"), point.ID); valid {
platformgasorder.ServeGasorderContractAttachment(ctx)
}
}
func CreateContract(ctx *gin.Context) {
point, station, ok := currentScope(ctx)
if !ok {
return
}
if !rewriteJSON(ctx, func(values map[string]any) bool {
userIdentity, _ := values["user_account_identity"].(string)
if _, _, valid := scopedUser(ctx, userIdentity, point.ID); !valid {
return false
}
values["gas_basic_identity"] = station.Identity
values["delivery_basic_identity"] = point.Identity
return true
}) {
return
}
platformgasorder.CreateGasorderContract(ctx)
}
func withContract(ctx *gin.Context, handler gin.HandlerFunc) {
point, _, ok := currentScope(ctx)
if ok {
if _, valid := scopedContract(ctx, ctx.Param("identity"), point.ID); valid {
handler(ctx)
}
}
}
func UpdateContract(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
if _, valid := scopedContract(ctx, ctx.Param("identity"), point.ID); !valid {
return
}
if !rewriteJSON(ctx, func(values map[string]any) bool {
values["delivery_basic_identity"] = point.Identity
return true
}) {
return
}
platformgasorder.UpdateGasorderContract(ctx)
}
func ActivateContract(ctx *gin.Context) { withContract(ctx, platformgasorder.ActivateGasorderContract) }
func RenewContract(ctx *gin.Context) { withContract(ctx, platformgasorder.RenewGasorderContract) }
func TerminateContract(ctx *gin.Context) {
withContract(ctx, platformgasorder.TerminateGasorderContract)
}
2026-08-23 11:30:52 +08:00
// deliveryContractProductQuery 构造当前配送点合同气瓶查询;指定合同时仅保留未解绑候选。
func deliveryContractProductQuery(database *gorm.DB, pointID, contractID uint64, keyword string) *gorm.DB {
query := common.ActiveRecords(database.Model(&models.GasorderContractProduct{})).
Select("gasorder_contract_product.*, COALESCE(display_product.name, '') AS product_name").
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_product.gasorder_contract_id").
Joins("LEFT JOIN product_info AS display_product ON display_product.id = gasorder_contract_product.product_info_id AND display_product.deleted_at IS NULL AND display_product.status <> ?", common.StatusArchived).
Where("gasorder_contract.delivery_basic_id = ?", pointID)
if contractID > 0 {
query = query.Where("gasorder_contract_product.gasorder_contract_id = ? AND gasorder_contract_product.unbound_at IS NULL", contractID)
}
keyword = strings.ToLower(strings.TrimSpace(keyword))
if keyword == "" {
return query
}
pattern := "%" + keyword + "%"
return query.Where("(LOWER(COALESCE(display_product.name, '')) LIKE ? OR LOWER(gasorder_contract_product.product_code) LIKE ? OR LOWER(gasorder_contract_product.product_type_name) LIKE ?)", pattern, pattern, pattern)
}
func ListContractProduct(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
2026-08-23 11:30:52 +08:00
contractIdentity := strings.TrimSpace(ctx.Query("contract_identity"))
var contractID uint64
if contractIdentity != "" {
contract, valid := scopedContract(ctx, contractIdentity, point.ID)
if !valid {
return
}
contractID = contract.ID
}
query := deliveryContractProductQuery(db(), point.ID, contractID, ctx.Query("keyword"))
page, size := common.PageSize(ctx)
var total int64
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
type contractProductDisplay struct {
models.GasorderContractProduct
ProductName string `gorm:"column:product_name" json:"product_name"`
}
var list []contractProductDisplay
if err := query.Order("gasorder_contract_product.created_at desc").Offset((page - 1) * size).Limit(size).Scan(&list).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
response, err := common.PublicResourceResponse(list)
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{
"total": total,
"list": common.ProtectPreciseLocation(ctx, &models.GasorderContractProduct{}, response),
})
}
func GetContractProduct(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
var item models.GasorderContractProduct
query := common.ActiveRecords(db().Model(&models.GasorderContractProduct{})).
Select("gasorder_contract_product.*").
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_product.gasorder_contract_id").
Where("gasorder_contract_product.identity = ? AND gasorder_contract.delivery_basic_id = ?", ctx.Param("identity"), point.ID)
respondRecord(ctx, query, &item)
}
func BindContractProduct(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
if !rewriteJSON(ctx, func(values map[string]any) bool {
identity, _ := values["gasorder_contract_identity"].(string)
_, valid := scopedContract(ctx, identity, point.ID)
return valid
}) {
return
}
platformgasorder.BindGasorderContractProduct(ctx)
}
func UnbindContractProduct(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
var count int64
err := db().Model(&models.GasorderContractProduct{}).
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_product.gasorder_contract_id").
Where("gasorder_contract_product.identity = ? AND gasorder_contract.delivery_basic_id = ?", ctx.Param("identity"), point.ID).
Count(&count).Error
if err != nil || count != 1 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
platformgasorder.UnbindGasorderContractProduct(ctx)
}
func ListContractRevision(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
query := common.ActiveRecords(db().Model(&models.GasorderContractRevision{})).
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_revision.gasorder_contract_id").
Where("gasorder_contract.delivery_basic_id = ?", point.ID)
listScoped(ctx, &models.GasorderContractRevision{}, query, "gasorder_contract_revision.created_at desc")
}
func GetContractRevision(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
var item models.GasorderContractRevision
query := common.ActiveRecords(db().Model(&models.GasorderContractRevision{})).
Select("gasorder_contract_revision.*").
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_revision.gasorder_contract_id").
Where("gasorder_contract_revision.identity = ? AND gasorder_contract.delivery_basic_id = ?", ctx.Param("identity"), point.ID)
respondRecord(ctx, query, &item)
}
func ListProductCandidate(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
query := common.ActiveRecords(db().Model(&models.ProductInfo{})).
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = product_info.user_account_id AND user_service_relation.status <> ?",
common.StatusArchived).Where("user_service_relation.delivery_basic_id = ?", point.ID)
listScoped(ctx, &models.ProductInfo{}, query, "product_info.created_at desc")
}
func GetProductCandidate(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
var item models.ProductInfo
query := common.ActiveRecords(db().Model(&models.ProductInfo{})).
Select("product_info.*").
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = product_info.user_account_id AND user_service_relation.status <> ?",
common.StatusArchived).
Where("product_info.identity = ? AND user_service_relation.delivery_basic_id = ?", ctx.Param("identity"), point.ID)
respondRecord(ctx, query, &item)
}
func scopedOrder(ctx *gin.Context, identity string, pointID uint64) (models.GasorderBasic, bool) {
var order models.GasorderBasic
if err := common.ActiveRecords(db()).Where("identity = ? AND delivery_basic_id = ?", identity, pointID).First(&order).Error; err != nil {
common.RespondRecordError(ctx, err)
return order, false
}
return order, true
}
func ListOrder(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if ok {
listScoped(ctx, &models.GasorderBasic{}, common.ActiveRecords(db().Model(&models.GasorderBasic{})).
Where("delivery_basic_id = ?", point.ID), "gasorder_basic.created_at desc")
}
}
func GetOrder(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if ok {
if _, valid := scopedOrder(ctx, ctx.Param("identity"), point.ID); valid {
platformgasorder.GetGasorderBasic(ctx)
}
}
}
func CreateOrder(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
if !rewriteJSON(ctx, func(values map[string]any) bool {
identity, _ := values["gasorder_contract_identity"].(string)
if _, valid := scopedContract(ctx, identity, point.ID); !valid {
return false
}
values["creator_type"] = "delivery"
values["creator_identity"] = point.Identity
return true
}) {
return
}
platformgasorder.CreateGasorderBasic(ctx)
}
func AssignOrder(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
if _, valid := scopedOrder(ctx, ctx.Param("identity"), point.ID); !valid {
return
}
if !rewriteJSON(ctx, func(values map[string]any) bool {
staffIdentity, _ := values["staff_account_identity"].(string)
staff, valid := scopedStaff(ctx, staffIdentity, point)
if !valid || staff.WorkStatus != "on_duty" || staff.Status != common.StatusEnable {
return false
}
var credentialCount int64
err := common.ActiveRecords(db().Model(&models.StaffCredential{})).
Where("staff_account_id = ? AND (expired_at IS NULL OR expired_at > ?)", staff.ID, time.Now()).
Count(&credentialCount).Error
if err != nil || credentialCount == 0 {
return false
}
values["delivery_basic_identity"] = point.Identity
return true
}) {
return
}
platformgasorder.AssignGasorderBasic(ctx)
}
func withOrder(ctx *gin.Context, handler gin.HandlerFunc) {
point, _, ok := currentScope(ctx)
if ok {
if _, valid := scopedOrder(ctx, ctx.Param("identity"), point.ID); valid {
handler(ctx)
}
}
}
func OrderException(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderException) }
func OrderRecover(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderRecover) }
func OrderCancel(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderCancel) }
func ReclaimOrder(ctx *gin.Context) {
account, point, _, ok := CurrentDeliveryAccount(ctx)
if !ok {
return
}
var request struct {
Reason string `json:"reason" binding:"required,max=1000"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
err := db().Transaction(func(tx *gorm.DB) error {
var order models.GasorderBasic
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ? AND delivery_basic_id = ?", ctx.Param("identity"), point.ID).First(&order).Error; err != nil {
return err
}
if order.OrderStatus != common.StatusAssigned {
return errors.New("only assigned order can be reclaimed")
}
if err := tx.Model(&order).Updates(map[string]any{
"staff_account_id": 0, "order_status": common.StatusCreated,
}).Error; err != nil {
return err
}
return tx.Create(&models.GasorderStatus{
Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID,
FromStatus: common.StatusAssigned, ToStatus: common.StatusCreated,
OperatorIdentity: account.Identity, OperatorName: account.DisplayName,
OccurredAt: time.Now(), Reason: request.Reason,
}).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "order_status": common.StatusCreated})
}
func AdjustOrderAmount(ctx *gin.Context) {
point, _, ok := currentScope(ctx)
if !ok {
return
}
var request struct {
DeliveryFee int64 `json:"delivery_fee"`
DiscountAmount int64 `json:"discount_amount"`
Reason string `json:"reason" binding:"required,max=1000"`
}
if err := ctx.ShouldBindJSON(&request); err != nil || request.DeliveryFee < 0 || request.DiscountAmount < 0 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
account, _, _, _ := CurrentDeliveryAccount(ctx)
err := db().Transaction(func(tx *gorm.DB) error {
var order models.GasorderBasic
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ? AND delivery_basic_id = ?", ctx.Param("identity"), point.ID).First(&order).Error; err != nil {
return err
}
if order.OrderStatus != common.StatusCreated && order.OrderStatus != common.StatusAssigned {
return errors.New("order amount cannot be adjusted")
}
var paymentCount int64
if err := tx.Model(&models.GasorderPayment{}).
Joins("JOIN payment_order ON payment_order.id = gasorder_payment.payment_order_id").
Where("gasorder_payment.gasorder_basic_id = ? AND payment_order.payment_status = ?", order.ID, 23).
Count(&paymentCount).Error; err != nil || paymentCount > 0 {
return errors.New("paid order cannot be adjusted")
}
if order.ProductAmount > math.MaxInt64-request.DeliveryFee {
return errors.New("payable amount overflow")
}
payable := order.ProductAmount + request.DeliveryFee - request.DiscountAmount
if payable <= 0 {
return errors.New("payable amount must be positive")
}
if err := tx.Model(&order).Updates(map[string]any{
"delivery_fee": request.DeliveryFee, "discount_amount": request.DiscountAmount, "payable_amount": payable,
}).Error; err != nil {
return err
}
record := models.GasorderStatus{
Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID,
FromStatus: order.OrderStatus, ToStatus: order.OrderStatus,
OperatorIdentity: account.Identity, OperatorName: account.DisplayName, OccurredAt: time.Now(),
Reason: fmt.Sprintf("%s配送费 %d→%d优惠金额 %d→%d应付金额 %d→%d",
request.Reason, order.DeliveryFee, request.DeliveryFee, order.DiscountAmount, request.DiscountAmount,
order.PayableAmount, payable),
}
return tx.Create(&record).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true})
}