feat: add Flutter mobile clients and staff delivery API

This commit is contained in:
david
2026-07-30 21:47:41 +08:00
parent 550efb3812
commit 36a5ced1c0
228 changed files with 17159 additions and 22 deletions

View File

@@ -3,6 +3,7 @@ package staff
import (
"strings"
"time"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
@@ -63,6 +64,57 @@ func Profile(ctx *gin.Context) {
})
}
// Preflight 返回当前单角色账号可由服务端确认的作业前置条件。
func Preflight(ctx *gin.Context) {
account, ok := clientcommon.StaffAccount(ctx)
if !ok {
return
}
var credential models.StaffCredential
credentialFound := impl.DBService.
Where("staff_account_id = ? AND status = ?", account.ID, base.StatusEnable).
Order("expired_at desc").
First(&credential).Error == nil
credentialValid := credentialFound && (credential.ExpiredAt == nil || credential.ExpiredAt.After(time.Now()))
organizationIdentity, organizationName, organizationType := "", "", ""
if account.DeliveryBasicID != 0 {
var organization models.DeliveryBasic
if impl.DBService.First(&organization, account.DeliveryBasicID).Error == nil {
organizationIdentity, organizationName, organizationType = organization.Identity, organization.Name, "delivery"
}
} else if account.GasBasicID != 0 {
var organization models.GasBasic
if impl.DBService.First(&organization, account.GasBasicID).Error == nil {
organizationIdentity, organizationName, organizationType = organization.Identity, organization.Name, "gas"
}
}
checks := gin.H{
"account": gin.H{"status": "passed"},
"role": gin.H{"status": "passed", "role_code": account.RoleCode},
"organization": gin.H{"status": checkStatus(organizationIdentity != ""), "identity": organizationIdentity, "name": organizationName, "type": organizationType},
"credential": gin.H{"status": checkStatus(credentialValid), "expired_at": credential.ExpiredAt},
"attendance": gin.H{"status": checkStatus(account.WorkStatus == "on_duty"), "work_status": account.WorkStatus},
"daily_training": gin.H{"status": "not_configured"},
"service_area": gin.H{"status": "not_configured"},
"authorized_device": gin.H{"status": "not_configured"},
}
infra.Response.Success(ctx, gin.H{
"role_code": account.RoleCode, "work_status": account.WorkStatus,
"can_work": organizationIdentity != "" && credentialValid && account.WorkStatus == "on_duty",
"checks": checks,
})
}
func checkStatus(passed bool) string {
if passed {
return "passed"
}
return "blocked"
}
// ChangePassword 修改当前工作人员登录密码。
func ChangePassword(ctx *gin.Context) {
account, ok := clientcommon.StaffAccount(ctx)

View File

@@ -0,0 +1,439 @@
package staff
import (
"math"
"strconv"
"strings"
"time"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
clientcommon "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/client/common"
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// ListDeliveryOrders 返回仅分派给当前配送人员的订单。
func ListDeliveryOrders(ctx *gin.Context) {
account, ok := requireDeliveryAccount(ctx)
if !ok {
return
}
var orders []models.GasorderBasic
if err := impl.DBService.Where("staff_account_id = ? AND status <> ?", account.ID, base.StatusArchived).
Order("created_at desc").Find(&orders).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, deliveryOrderResponses(orders))
}
// GetDeliveryOrder 返回当前配送人员订单详情及气瓶项目。
func GetDeliveryOrder(ctx *gin.Context) {
account, ok := requireDeliveryAccount(ctx)
if !ok {
return
}
order, ok := requireDeliveryOrder(ctx, account, false)
if !ok {
return
}
var items []models.GasorderItem
if err := impl.DBService.Where("gasorder_basic_id = ?", order.ID).Order("created_at asc").Find(&items).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"order": deliveryOrderResponse(order), "items": base.ResourceResponse(items)})
}
// StartDeliveryOrder 将已就绪订单置为配送中并创建本次轨迹。
func StartDeliveryOrder(ctx *gin.Context) {
transitionDeliveryOrder(ctx, base.StatusReady, base.StatusDelivering, true)
}
// AppendDeliveryTracks 批量补传配送中轨迹点request_no 保证重复补传不重复落库。
func AppendDeliveryTracks(ctx *gin.Context) {
account, ok := requireDeliveryAccount(ctx)
if !ok {
return
}
var request struct {
Points []deliveryTrackPointRequest `json:"points" binding:"required,min=1,max=100"`
}
if ctx.ShouldBindJSON(&request) != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
order, ok := requireDeliveryOrder(ctx, account, true)
if !ok || order.OrderStatus != base.StatusDelivering {
if ok {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
}
return
}
var track models.GasorderTrack
if impl.DBService.Where("gasorder_basic_id = ? AND staff_account_id = ? AND completed_at IS NULL", order.ID, account.ID).
First(&track).Error != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
receivedAt := time.Now()
points := make([]models.GasorderTrackPoint, 0, len(request.Points))
for _, item := range request.Points {
if !validTrackPoint(item) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
points = append(points, models.GasorderTrackPoint{
Entity: base.NewEntity(base.StatusEnable), GasorderTrackID: track.ID, RequestNo: item.RequestNo,
Longitude: item.Longitude, Latitude: item.Latitude, OccurredAt: item.OccurredAt,
ReceivedAt: receivedAt, Source: item.Source, Accuracy: item.Accuracy,
Speed: item.Speed, Direction: item.Direction,
})
}
if err := impl.DBService.Clauses(clause.OnConflict{DoNothing: true}).Create(&points).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"accepted": len(points)})
}
// ArriveDeliveryOrder 校验地理围栏并把配送单推进到待签收。
func ArriveDeliveryOrder(ctx *gin.Context) {
account, ok := requireDeliveryAccount(ctx)
if !ok {
return
}
var request deliveryTrackPointRequest
if ctx.ShouldBindJSON(&request) != nil || !validTrackPoint(request) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
order, ok := requireDeliveryOrder(ctx, account, true)
if !ok {
return
}
distance, valid := coordinateDistanceMeters(order.Longitude, order.Latitude, request.Longitude, request.Latitude)
if order.OrderStatus != base.StatusDelivering || !valid || distance > config.Spec.Global.DeliveryArrivalRadiusMeters {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
var track models.GasorderTrack
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("gasorder_basic_id = ? AND staff_account_id = ? AND completed_at IS NULL", order.ID, account.ID).
First(&track).Error; err != nil {
return err
}
now := time.Now()
point := models.GasorderTrackPoint{
Entity: base.NewEntity(base.StatusEnable), GasorderTrackID: track.ID, RequestNo: request.RequestNo,
Longitude: request.Longitude, Latitude: request.Latitude, OccurredAt: request.OccurredAt,
ReceivedAt: now, Source: request.Source, Accuracy: request.Accuracy, Speed: request.Speed, Direction: request.Direction,
}
if err := tx.Create(&point).Error; err != nil {
return err
}
if err := tx.Model(&track).Update("completed_at", &now).Error; err != nil {
return err
}
result := tx.Model(&models.GasorderBasic{}).
Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, base.StatusDelivering).
Update("order_status", base.StatusAwaitingConfirmation)
if result.Error != nil || result.RowsAffected != 1 {
return gorm.ErrInvalidData
}
return tx.Create(deliveryStatusRecord(order, account, base.StatusDelivering, base.StatusAwaitingConfirmation, "配送到达")).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"order_status": base.StatusAwaitingConfirmation, "distance_meters": math.Round(distance)})
}
// ExceptionDeliveryOrder 将配送中或待签收订单置为异常。
func ExceptionDeliveryOrder(ctx *gin.Context) {
account, ok := requireDeliveryAccount(ctx)
if !ok {
return
}
var request struct {
Reason string `json:"reason" binding:"required,max=1000"`
}
if ctx.ShouldBindJSON(&request) != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
order, ok := requireDeliveryOrder(ctx, account, true)
if !ok || (order.OrderStatus != base.StatusDelivering && order.OrderStatus != base.StatusAwaitingConfirmation) {
if ok {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
}
return
}
updateDeliveryStatus(ctx, order, account, base.StatusException, request.Reason, gin.H{"previous_order_status": order.OrderStatus})
}
// RecoverDeliveryOrder 将本人异常订单恢复到异常前状态。
func RecoverDeliveryOrder(ctx *gin.Context) {
account, ok := requireDeliveryAccount(ctx)
if !ok {
return
}
var request struct {
Reason string `json:"reason" binding:"required,max=1000"`
}
if ctx.ShouldBindJSON(&request) != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
order, ok := requireDeliveryOrder(ctx, account, true)
if !ok || order.OrderStatus != base.StatusException ||
(order.PreviousOrderStatus != base.StatusDelivering && order.PreviousOrderStatus != base.StatusAwaitingConfirmation) {
if ok {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
}
return
}
target := order.PreviousOrderStatus
updateDeliveryStatus(ctx, order, account, target, request.Reason, gin.H{"previous_order_status": base.StatusDraft})
}
// SubmitDeliveryReceipt 保存签收凭证并完成订单,重复 request_no 返回既有结果。
func SubmitDeliveryReceipt(ctx *gin.Context) {
account, ok := requireDeliveryAccount(ctx)
if !ok {
return
}
var request struct {
RequestNo string `json:"request_no" binding:"required"`
ConfirmType string `json:"confirm_type" binding:"required,oneof=signature receipt_code"`
RecipientName string `json:"recipient_name" binding:"required,max=64"`
RecipientPhone string `json:"recipient_phone" binding:"max=32"`
ProofURI string `json:"proof_uri" binding:"required,max=512"`
Remark string `json:"remark" binding:"max=1000"`
}
if ctx.ShouldBindJSON(&request) != nil || !strings.HasPrefix(request.ProofURI, "/uploads/") {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var existing models.GasorderConfirm
if impl.DBService.Where("request_no = ?", request.RequestNo).First(&existing).Error == nil {
infra.Response.Success(ctx, gin.H{"confirmed": true, "identity": existing.Identity, "order_status": base.StatusCompleted})
return
}
order, ok := requireDeliveryOrder(ctx, account, true)
if !ok || order.OrderStatus != base.StatusAwaitingConfirmation {
if ok {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
}
return
}
confirm := models.GasorderConfirm{
Entity: base.NewEntity(base.StatusEnable), GasorderBasicID: order.ID, RequestNo: request.RequestNo,
ConfirmType: request.ConfirmType, RecipientName: request.RecipientName, RecipientPhone: request.RecipientPhone,
ProofURI: request.ProofURI, ConfirmedAt: time.Now(), Remark: request.Remark,
}
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&confirm).Error; err != nil {
return err
}
result := tx.Model(&models.GasorderBasic{}).
Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, base.StatusAwaitingConfirmation).
Update("order_status", base.StatusCompleted)
if result.Error != nil || result.RowsAffected != 1 {
return gorm.ErrInvalidData
}
if err := tx.Model(&models.GasorderItem{}).Where("gasorder_basic_id = ?", order.ID).Update("active", false).Error; err != nil {
return err
}
return tx.Create(deliveryStatusRecord(order, account, base.StatusAwaitingConfirmation, base.StatusCompleted, "用户签收")).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"confirmed": true, "identity": confirm.Identity, "order_status": base.StatusCompleted})
}
type deliveryTrackPointRequest struct {
RequestNo string `json:"request_no" binding:"required"`
Longitude string `json:"longitude" binding:"required"`
Latitude string `json:"latitude" binding:"required"`
OccurredAt time.Time `json:"occurred_at" binding:"required"`
Source string `json:"source" binding:"required,oneof=gps network manual"`
Accuracy string `json:"accuracy"`
Speed string `json:"speed"`
Direction string `json:"direction"`
}
func requireDeliveryAccount(ctx *gin.Context) (models.StaffAccount, bool) {
account, ok := clientcommon.StaffAccount(ctx)
if !ok {
return account, false
}
if account.RoleCode != "delivery" {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
return account, false
}
return account, true
}
func requireDeliveryOrder(ctx *gin.Context, account models.StaffAccount, lock bool) (models.GasorderBasic, bool) {
var order models.GasorderBasic
query := impl.DBService
if lock {
query = query.Clauses(clause.Locking{Strength: "UPDATE"})
}
if query.Where("identity = ? AND staff_account_id = ? AND status <> ?", ctx.Param("identity"), account.ID, base.StatusArchived).
First(&order).Error != nil {
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
return order, false
}
return order, true
}
func transitionDeliveryOrder(ctx *gin.Context, from, to int, createTrack bool) {
account, ok := requireDeliveryAccount(ctx)
if !ok {
return
}
var request struct {
Reason string `json:"reason" binding:"required,max=1000"`
}
if ctx.ShouldBindJSON(&request) != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
order, ok := requireDeliveryOrder(ctx, account, true)
if !ok || order.OrderStatus != from {
if ok {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
}
return
}
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
result := tx.Model(&models.GasorderBasic{}).
Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, from).
Update("order_status", to)
if result.Error != nil || result.RowsAffected != 1 {
return gorm.ErrInvalidData
}
if createTrack {
var attempt int
if err := tx.Model(&models.GasorderTrack{}).Where("gasorder_basic_id = ?", order.ID).
Select("COALESCE(MAX(attempt_no), 0)").Scan(&attempt).Error; err != nil {
return err
}
if err := tx.Create(&models.GasorderTrack{
Entity: base.NewEntity(base.StatusEnable), GasorderBasicID: order.ID,
StaffAccountID: account.ID, AttemptNo: attempt + 1, StartedAt: time.Now(),
}).Error; err != nil {
return err
}
}
return tx.Create(deliveryStatusRecord(order, account, from, to, request.Reason)).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"order_status": to})
}
func updateDeliveryStatus(ctx *gin.Context, order models.GasorderBasic, account models.StaffAccount, target int, reason string, extra gin.H) {
updates := gin.H{"order_status": target}
for key, value := range extra {
updates[key] = value
}
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
result := tx.Model(&models.GasorderBasic{}).
Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, order.OrderStatus).
Updates(updates)
if result.Error != nil || result.RowsAffected != 1 {
return gorm.ErrInvalidData
}
return tx.Create(deliveryStatusRecord(order, account, order.OrderStatus, target, reason)).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"order_status": target})
}
func deliveryStatusRecord(order models.GasorderBasic, account models.StaffAccount, from, to int, reason string) models.GasorderStatus {
return models.GasorderStatus{
Entity: base.NewEntity(base.StatusEnable), GasorderBasicID: order.ID,
FromStatus: from, ToStatus: to, OperatorIdentity: account.Identity,
OperatorName: account.Name, OccurredAt: time.Now(), Reason: strings.TrimSpace(reason),
}
}
func deliveryOrderResponses(orders []models.GasorderBasic) []gin.H {
responses := make([]gin.H, 0, len(orders))
for _, order := range orders {
responses = append(responses, deliveryOrderResponse(order))
}
return responses
}
func deliveryOrderResponse(order models.GasorderBasic) gin.H {
return gin.H{
"identity": order.Identity, "order_no": order.OrderNo, "order_status": order.OrderStatus,
"address": order.Address, "longitude": order.Longitude, "latitude": order.Latitude,
"contact_name": order.ContactName, "contact_phone": order.ContactPhone,
"payable_amount": order.PayableAmount, "remark": order.Remark,
"created_at": order.CreatedAt, "updated_at": order.UpdatedAt,
"allowed_actions": deliveryAllowedActions(order.OrderStatus),
}
}
func deliveryAllowedActions(status int) []string {
switch status {
case base.StatusReady:
return []string{"start"}
case base.StatusDelivering:
return []string{"append_tracks", "arrive", "exception"}
case base.StatusAwaitingConfirmation:
return []string{"submit_receipt", "exception"}
case base.StatusException:
return []string{"recover"}
default:
return []string{}
}
}
func validTrackPoint(point deliveryTrackPointRequest) bool {
_, longitudeOK := parseCoordinate(point.Longitude, -180, 180)
_, latitudeOK := parseCoordinate(point.Latitude, -90, 90)
return point.RequestNo != "" && longitudeOK && latitudeOK && !point.OccurredAt.IsZero()
}
func coordinateDistanceMeters(longitudeA, latitudeA, longitudeB, latitudeB string) (float64, bool) {
lonA, okA := parseCoordinate(longitudeA, -180, 180)
latA, okB := parseCoordinate(latitudeA, -90, 90)
lonB, okC := parseCoordinate(longitudeB, -180, 180)
latB, okD := parseCoordinate(latitudeB, -90, 90)
if !(okA && okB && okC && okD) {
return 0, false
}
const earthRadiusMeters = 6371000
latitudeDelta := (latB - latA) * math.Pi / 180
longitudeDelta := (lonB - lonA) * math.Pi / 180
a := math.Sin(latitudeDelta/2)*math.Sin(latitudeDelta/2) +
math.Cos(latA*math.Pi/180)*math.Cos(latB*math.Pi/180)*
math.Sin(longitudeDelta/2)*math.Sin(longitudeDelta/2)
return earthRadiusMeters * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)), true
}
func parseCoordinate(value string, minimum, maximum float64) (float64, bool) {
parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
return parsed, err == nil && parsed >= minimum && parsed <= maximum && !math.IsNaN(parsed) && !math.IsInf(parsed, 0)
}

View File

@@ -0,0 +1,55 @@
package staff
import (
"testing"
"time"
base "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
)
func TestDeliveryAllowedActions(t *testing.T) {
tests := map[int][]string{
base.StatusReady: {"start"},
base.StatusDelivering: {"append_tracks", "arrive", "exception"},
base.StatusAwaitingConfirmation: {"submit_receipt", "exception"},
base.StatusException: {"recover"},
}
for status, expected := range tests {
actual := deliveryAllowedActions(status)
if len(actual) != len(expected) {
t.Fatalf("status %d actions = %v, want %v", status, actual, expected)
}
for index := range expected {
if actual[index] != expected[index] {
t.Fatalf("status %d actions = %v, want %v", status, actual, expected)
}
}
}
if actions := deliveryAllowedActions(base.StatusCompleted); len(actions) != 0 {
t.Fatalf("completed order exposed actions: %v", actions)
}
}
func TestCoordinateDistanceMeters(t *testing.T) {
distance, ok := coordinateDistanceMeters("121.5500", "31.2250", "121.5501", "31.2251")
if !ok || distance <= 0 || distance >= 20 {
t.Fatalf("unexpected nearby distance: %f, valid=%v", distance, ok)
}
if _, ok := coordinateDistanceMeters("invalid", "31.2250", "121.5501", "31.2251"); ok {
t.Fatal("invalid coordinate accepted")
}
}
func TestValidTrackPoint(t *testing.T) {
point := deliveryTrackPointRequest{
RequestNo: "track-1", Longitude: "121.55", Latitude: "31.22",
OccurredAt: time.Now(), Source: "gps",
}
if !validTrackPoint(point) {
t.Fatal("valid track point rejected")
}
point.Latitude = "91"
if validTrackPoint(point) {
t.Fatal("out-of-range latitude accepted")
}
}

View File

@@ -92,6 +92,21 @@ func ListTickets(ctx *gin.Context) {
infra.Response.Success(ctx, base.ResourceResponse(list))
}
// GetTicket 按公开 identity 返回当前工作人员被分派的单一工单。
func GetTicket(ctx *gin.Context) {
account, ok := clientcommon.StaffAccount(ctx)
if !ok {
return
}
var ticket models.CsTicket
if impl.DBService.Where("identity = ? AND staff_account_id = ? AND status <> ?", ctx.Param("identity"), account.ID, base.StatusArchived).
First(&ticket).Error != nil {
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
return
}
infra.Response.Success(ctx, base.ResourceResponse(ticket))
}
// StartTicket 将本人已分派工单置为处理中。
func StartTicket(ctx *gin.Context) {
updateTicketStatus(ctx, 18, 11, nil)

View File

@@ -9,13 +9,14 @@ import (
// GasorderConfirm 对应 gasorder_confirm保存用户签收确认。
type GasorderConfirm struct {
Entity // 公共实体字段
GasorderBasicID uint64 `gorm:"column:gasorder_basic_id;not null;uniqueIndex" json:"gasorder_basic_id"` // 订单自增主键
ConfirmType string `gorm:"column:confirm_type;type:varchar(32);not null" json:"confirm_type"` // 签收类型
RecipientName string `gorm:"column:recipient_name;type:varchar(64);not null" json:"recipient_name"` // 签收人姓名快照
RecipientPhone string `gorm:"column:recipient_phone;type:varchar(32);not null;default:''" json:"recipient_phone"` // 签收人手机号快照
ProofURI string `gorm:"column:proof_uri;type:varchar(512);not null;default:''" json:"proof_uri"` // 签名或凭证地址
ConfirmedAt time.Time `gorm:"column:confirmed_at;type:timestamptz;not null;index" json:"confirmed_at"` // 确认时间
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 签收备注
GasorderBasicID uint64 `gorm:"column:gasorder_basic_id;not null;uniqueIndex" json:"gasorder_basic_id"` // 订单自增主键
RequestNo string `gorm:"column:request_no;type:varchar(128);not null;default:'';uniqueIndex:,where:request_no <> ''" json:"request_no"` // 客户端提交幂等号
ConfirmType string `gorm:"column:confirm_type;type:varchar(32);not null" json:"confirm_type"` // 签收类型
RecipientName string `gorm:"column:recipient_name;type:varchar(64);not null" json:"recipient_name"` // 签收人姓名快照
RecipientPhone string `gorm:"column:recipient_phone;type:varchar(32);not null;default:''" json:"recipient_phone"` // 签收人手机号快照
ProofURI string `gorm:"column:proof_uri;type:varchar(512);not null;default:''" json:"proof_uri"` // 签名或凭证地址
ConfirmedAt time.Time `gorm:"column:confirmed_at;type:timestamptz;not null;index" json:"confirmed_at"` // 确认时间
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 签收备注
}
func init() { database.AppendMigrate(&GasorderConfirm{}) }

View File

@@ -62,13 +62,23 @@ func registerStaffClient(serviceKey string, engine *gin.Engine) {
protected := engine.Group(basePath)
protected.Use(sdkmiddleware.JwtAuth(true), clientcommon.RequireClient("service_app"))
protected.GET("/auth/profile", stafflogic.Profile)
protected.GET("/preflight", stafflogic.Preflight)
protected.PUT("/auth/password", stafflogic.ChangePassword)
protected.POST("/attendance", stafflogic.Attendance)
protected.GET("/tickets", stafflogic.ListTickets)
protected.GET("/tickets/:identity", stafflogic.GetTicket)
protected.POST("/tickets/:identity/start", stafflogic.StartTicket)
protected.POST("/tickets/:identity/exception", stafflogic.ExceptionTicket)
protected.POST("/tickets/:identity/recover", stafflogic.RecoverTicket)
protected.POST("/tickets/:identity/submit-result", stafflogic.SubmitTicketResult)
protected.GET("/delivery/orders", stafflogic.ListDeliveryOrders)
protected.GET("/delivery/orders/:identity", stafflogic.GetDeliveryOrder)
protected.POST("/delivery/orders/:identity/start", stafflogic.StartDeliveryOrder)
protected.POST("/delivery/orders/:identity/tracks", stafflogic.AppendDeliveryTracks)
protected.POST("/delivery/orders/:identity/arrive", stafflogic.ArriveDeliveryOrder)
protected.POST("/delivery/orders/:identity/exception", stafflogic.ExceptionDeliveryOrder)
protected.POST("/delivery/orders/:identity/recover", stafflogic.RecoverDeliveryOrder)
protected.POST("/delivery/orders/:identity/submit-receipt", stafflogic.SubmitDeliveryReceipt)
registerClientWalletRoutes(protected, "service_app")
}

View File

@@ -12,13 +12,20 @@ func TestRegisterClientRoutes(t *testing.T) {
RegisterClient("heqi", engine)
expected := map[string]bool{
"POST /heqi/client/v1/user/auth/register": false,
"POST /heqi/client/v1/user/auth/login": false,
"POST /heqi/client/v1/user/wallet/recharges": false,
"POST /heqi/client/v1/user/shop/orders/:identity/pay": false,
"POST /heqi/client/v1/staff/auth/login": false,
"POST /heqi/client/v1/staff/attendance": false,
"POST /heqi/client/v1/staff/tickets/:identity/submit-result": false,
"POST /heqi/client/v1/user/auth/register": false,
"POST /heqi/client/v1/user/auth/login": false,
"POST /heqi/client/v1/user/wallet/recharges": false,
"POST /heqi/client/v1/user/shop/orders/:identity/pay": false,
"POST /heqi/client/v1/staff/auth/login": false,
"GET /heqi/client/v1/staff/preflight": false,
"POST /heqi/client/v1/staff/attendance": false,
"GET /heqi/client/v1/staff/tickets/:identity": false,
"POST /heqi/client/v1/staff/tickets/:identity/submit-result": false,
"GET /heqi/client/v1/staff/delivery/orders": false,
"POST /heqi/client/v1/staff/delivery/orders/:identity/start": false,
"POST /heqi/client/v1/staff/delivery/orders/:identity/tracks": false,
"POST /heqi/client/v1/staff/delivery/orders/:identity/arrive": false,
"POST /heqi/client/v1/staff/delivery/orders/:identity/submit-receipt": false,
}
for _, route := range engine.Routes() {
key := route.Method + " " + route.Path

View File

@@ -65,7 +65,7 @@ func MockData(database *gorm.DB) error {
staff := models.StaffAccount{
Entity: entity(5, common.StatusEnable), Username: "mock_driver", PasswordHash: string(passwordHash),
Name: "王师傅", Phone: "13900000001", RoleCode: "driver",
Name: "王师傅", Phone: "13900000001", RoleCode: "delivery",
GasBasicID: gas.ID, DeliveryBasicID: delivery.ID, WorkStatus: "on_duty",
}
if err := put(tx, &staff); err != nil {