2026-08-23 01:08:25 +08:00
// 功能描述:实现配送点范围内的合同、合同气瓶和配送订单接口。
2026-08-23 11:30:52 +08:00
// 版本: v1.2.0。
2026-07-30 14:16:58 +08:00
package delivery
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math"
2026-08-23 01:08:25 +08:00
"strings"
2026-07-30 14:16:58 +08:00
"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
}
2026-07-30 14:16:58 +08:00
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-07-30 14:16:58 +08:00
}
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
2026-07-30 14:16:58 +08:00
}
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 )
}
}
2026-07-30 14:16:58 +08:00
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 )
}
2026-07-30 14:16:58 +08:00
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 ) ,
} )
2026-07-30 14:16:58 +08:00
}
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 { } ) .
2026-08-02 23:24:32 +08:00
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 ) .
2026-07-30 14:16:58 +08:00
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 } )
}