feat: implement gas and delivery admin systems
This commit is contained in:
138
backend/api/internal/logic/delivery/auth.go
Normal file
138
backend/api/internal/logic/delivery/auth.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/crypto/token"
|
||||
"git.apinb.com/bsm-sdk/core/env"
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/bsm-sdk/core/middleware"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func Login(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var account models.DeliveryAccount
|
||||
if err := impl.DBService.Where("username = ?", strings.TrimSpace(request.Username)).First(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
var point models.DeliveryBasic
|
||||
var station models.GasBasic
|
||||
if account.Status != common.StatusEnable || account.RoleCode != "admin" ||
|
||||
impl.DBService.First(&point, account.DeliveryBasicID).Error != nil || point.Status != common.StatusEnable ||
|
||||
impl.DBService.First(&station, point.GasBasicID).Error != nil || station.Status != common.StatusEnable ||
|
||||
bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.Password)) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
accessToken, err := token.New(env.Runtime.JwtSecretKey).GenerateJwt(
|
||||
0, account.Identity, "delivery_admin", account.RoleCode, nil,
|
||||
map[string]string{"delivery_basic_identity": point.Identity, "gas_basic_identity": station.Identity, "username": account.Username, "display_name": account.DisplayName},
|
||||
)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{
|
||||
"access_token": accessToken, "token_type": "JWT", "identity": account.Identity,
|
||||
"display_name": account.DisplayName, "role_code": account.RoleCode,
|
||||
"delivery_basic_identity": point.Identity, "gas_basic_identity": station.Identity,
|
||||
})
|
||||
}
|
||||
|
||||
func RequireDeliveryAdmin() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err != nil || claims.Client != "delivery_admin" || claims.Role != "admin" || claims.Extend["delivery_basic_identity"] == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
err = impl.DBService.Model(&models.DeliveryAccount{}).
|
||||
Joins("JOIN delivery_basic ON delivery_basic.id = delivery_account.delivery_basic_id").
|
||||
Where("delivery_account.identity = ? AND delivery_account.status = ? AND delivery_account.role_code = ? AND delivery_basic.identity = ? AND delivery_basic.status = ?",
|
||||
claims.Identity, common.StatusEnable, "admin", claims.Extend["delivery_basic_identity"], common.StatusEnable).
|
||||
Count(&count).Error
|
||||
if err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CurrentDeliveryAccount(ctx *gin.Context) (models.DeliveryAccount, models.DeliveryBasic, models.GasBasic, bool) {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return models.DeliveryAccount{}, models.DeliveryBasic{}, models.GasBasic{}, false
|
||||
}
|
||||
var account models.DeliveryAccount
|
||||
var point models.DeliveryBasic
|
||||
var station models.GasBasic
|
||||
if err := impl.DBService.Where("identity = ?", claims.Identity).First(&account).Error; err != nil ||
|
||||
impl.DBService.First(&point, account.DeliveryBasicID).Error != nil ||
|
||||
impl.DBService.First(&station, point.GasBasicID).Error != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return account, point, station, false
|
||||
}
|
||||
return account, point, station, true
|
||||
}
|
||||
|
||||
func CurrentProfile(ctx *gin.Context) {
|
||||
account, point, station, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
menus := MenusForRole(account.RoleCode)
|
||||
codes := make([]string, 0, len(menus))
|
||||
for _, menu := range menus {
|
||||
codes = append(codes, menu.Identity)
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{
|
||||
"identity": account.Identity, "username": account.Username, "display_name": account.DisplayName,
|
||||
"role_code": account.RoleCode, "delivery_basic_identity": point.Identity, "delivery_basic_name": point.Name,
|
||||
"gas_basic_identity": station.Identity, "menu_codes": codes,
|
||||
})
|
||||
}
|
||||
|
||||
func ChangePassword(ctx *gin.Context) {
|
||||
account, _, _, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
CurrentPassword string `json:"current_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsValidAccountPassword(request.NewPassword) ||
|
||||
bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.CurrentPassword)) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := common.PasswordHash(request.NewPassword)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Model(&account).Update("password_hash", hash).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"changed": true})
|
||||
}
|
||||
89
backend/api/internal/logic/delivery/base.go
Normal file
89
backend/api/internal/logic/delivery/base.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
)
|
||||
|
||||
func currentScope(ctx *gin.Context) (models.DeliveryBasic, models.GasBasic, bool) {
|
||||
_, point, station, ok := CurrentDeliveryAccount(ctx)
|
||||
return point, station, ok
|
||||
}
|
||||
|
||||
func ListMenu(ctx *gin.Context) {
|
||||
account, _, _, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
menus := MenusForRole(account.RoleCode)
|
||||
infra.Response.Success(ctx, gin.H{"total": len(menus), "list": menus})
|
||||
}
|
||||
|
||||
func InvitationQRCode(ctx *gin.Context) {
|
||||
_, point, station, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
registerURL, _ := url.Parse(config.Spec.Global.UserRegisterURL)
|
||||
query := registerURL.Query()
|
||||
query.Set("gas_identity", station.Identity)
|
||||
query.Set("delivery_identity", point.Identity)
|
||||
registerURL.RawQuery = query.Encode()
|
||||
infra.Response.Success(ctx, gin.H{"register_url": registerURL.String()})
|
||||
}
|
||||
|
||||
func ListProfile(ctx *gin.Context) {
|
||||
_, point, _, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondList(ctx, []models.DeliveryBasic{point}, 1)
|
||||
}
|
||||
|
||||
func respondList(ctx *gin.Context, list any, total int64) {
|
||||
response, err := common.PublicResourceResponse(list)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": response})
|
||||
}
|
||||
|
||||
func listScoped(ctx *gin.Context, model any, query *gorm.DB, order string) {
|
||||
page, size := common.PageSize(ctx)
|
||||
query = common.ApplyKeywordFilter(ctx, query, model)
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
list := reflect.New(reflect.SliceOf(reflect.TypeOf(model).Elem()))
|
||||
if err := query.Order(order).Offset((page - 1) * size).Limit(size).Find(list.Interface()).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondList(ctx, list.Elem().Interface(), total)
|
||||
}
|
||||
|
||||
func respondRecord(ctx *gin.Context, query *gorm.DB, model any) {
|
||||
if err := query.First(model).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := common.PublicResourceResponse(model)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
func db() *gorm.DB { return impl.DBService }
|
||||
55
backend/api/internal/logic/delivery/dashboard.go
Normal file
55
backend/api/internal/logic/delivery/dashboard.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Dashboard(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
result := gin.H{}
|
||||
queries := []struct {
|
||||
key string
|
||||
model any
|
||||
where string
|
||||
args []any
|
||||
}{
|
||||
{"staff_count", &models.StaffAccount{}, "delivery_basic_id = ? AND role_code = ? AND status <> ?", []any{point.ID, "delivery", common.StatusArchived}},
|
||||
{"user_count", &models.UserServiceRelation{}, "delivery_basic_id = ? AND status <> ?", []any{point.ID, common.StatusArchived}},
|
||||
{"contract_count", &models.GasorderContract{}, "delivery_basic_id = ? AND status <> ?", []any{point.ID, common.StatusArchived}},
|
||||
{"order_count", &models.GasorderBasic{}, "delivery_basic_id = ? AND status <> ?", []any{point.ID, common.StatusArchived}},
|
||||
}
|
||||
for _, query := range queries {
|
||||
var count int64
|
||||
if err := db().Model(query.model).Where(query.where, query.args...).Count(&count).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
result[query.key] = count
|
||||
}
|
||||
infra.Response.Success(ctx, result)
|
||||
}
|
||||
|
||||
func DashboardReport(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
type row struct {
|
||||
Status int `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
var rows []row
|
||||
if err := db().Model(&models.GasorderBasic{}).Select("order_status AS status, count(*) AS count").
|
||||
Where("delivery_basic_id = ? AND status <> ?", point.ID, common.StatusArchived).
|
||||
Group("order_status").Order("order_status").Scan(&rows).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"orders_by_status": rows})
|
||||
}
|
||||
210
backend/api/internal/logic/delivery/finance.go
Normal file
210
backend/api/internal/logic/delivery/finance.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"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/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func currentWallet(ctx *gin.Context, point models.DeliveryBasic) (models.WalletBasic, bool) {
|
||||
var wallet models.WalletBasic
|
||||
if err := common.ActiveRecords(db()).Where("owner_type = ? AND owner_id = ? AND owner_identity = ?",
|
||||
"delivery", point.ID, point.Identity).First(&wallet).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return wallet, false
|
||||
}
|
||||
return wallet, true
|
||||
}
|
||||
|
||||
func ListWallet(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.WalletBasic{}, common.ActiveRecords(db().Model(&models.WalletBasic{})).
|
||||
Where("owner_type = ? AND owner_id = ?", "delivery", point.ID), "wallet_basic.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetWallet(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
respondRecord(ctx, common.ActiveRecords(db()).Where(
|
||||
"identity = ? AND owner_type = ? AND owner_id = ?", ctx.Param("identity"), "delivery", point.ID), &wallet)
|
||||
}
|
||||
|
||||
func listWalletChild(ctx *gin.Context, model any, table string) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(db().Model(model)).
|
||||
Joins("JOIN wallet_basic ON wallet_basic.id = "+table+".wallet_basic_id").
|
||||
Where("wallet_basic.owner_type = ? AND wallet_basic.owner_id = ?", "delivery", point.ID)
|
||||
listScoped(ctx, model, query, table+".created_at desc")
|
||||
}
|
||||
|
||||
func ListBank(ctx *gin.Context) { listWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func ListPayment(ctx *gin.Context) { listWalletChild(ctx, &models.WalletPayment{}, "wallet_payment") }
|
||||
func ListRecord(ctx *gin.Context) { listWalletChild(ctx, &models.WalletRecord{}, "wallet_record") }
|
||||
func ListRefund(ctx *gin.Context) { listWalletChild(ctx, &models.WalletRefund{}, "wallet_refund") }
|
||||
func ListApplyCash(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletApplyCash{}, "wallet_apply_cash")
|
||||
}
|
||||
|
||||
func getWalletChild(ctx *gin.Context, model any, table string) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(db().Model(model)).
|
||||
Joins("JOIN wallet_basic ON wallet_basic.id = "+table+".wallet_basic_id").
|
||||
Where(table+".identity = ? AND wallet_basic.owner_type = ? AND wallet_basic.owner_id = ?",
|
||||
ctx.Param("identity"), "delivery", point.ID)
|
||||
respondRecord(ctx, query, model)
|
||||
}
|
||||
|
||||
func GetBank(ctx *gin.Context) { getWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func GetPayment(ctx *gin.Context) { getWalletChild(ctx, &models.WalletPayment{}, "wallet_payment") }
|
||||
func GetRecord(ctx *gin.Context) { getWalletChild(ctx, &models.WalletRecord{}, "wallet_record") }
|
||||
func GetRefund(ctx *gin.Context) { getWalletChild(ctx, &models.WalletRefund{}, "wallet_refund") }
|
||||
func GetApplyCash(ctx *gin.Context) {
|
||||
getWalletChild(ctx, &models.WalletApplyCash{}, "wallet_apply_cash")
|
||||
}
|
||||
|
||||
func Recharge(ctx *gin.Context) {
|
||||
account, point, _, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
RequestNo string `json:"request_no" binding:"required,max=128"`
|
||||
Amount int64 `json:"amount" binding:"required"`
|
||||
Reason string `json:"reason" binding:"required,max=1000"`
|
||||
Remark string `json:"remark" binding:"max=2000"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Amount <= 0 ||
|
||||
request.Amount > config.Spec.Global.ManualRechargeMaxAmount ||
|
||||
strings.TrimSpace(request.RequestNo) == "" || strings.TrimSpace(request.Reason) == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var record models.WalletRecord
|
||||
err := db().Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("request_no = ?", request.RequestNo).First(&record).Error; err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("owner_type = ? AND owner_id = ? AND owner_identity = ?", "delivery", point.ID, point.Identity).
|
||||
First(&wallet).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.Model(&wallet).Where("status = ? AND balance <= ?", common.StatusEnable, math.MaxInt64-request.Amount).
|
||||
Update("balance", gorm.Expr("balance + ?", request.Amount))
|
||||
if result.Error != nil || result.RowsAffected != 1 {
|
||||
return errors.New("wallet disabled or balance overflow")
|
||||
}
|
||||
if err := tx.First(&wallet, wallet.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
record = models.WalletRecord{
|
||||
Entity: common.NewEntity(common.StatusEnable), WalletBasicID: wallet.ID,
|
||||
RecordNo: models.NewIdentity(), RequestNo: request.RequestNo, Direction: "income",
|
||||
TradeType: "delivery_admin_recharge", Amount: request.Amount,
|
||||
BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance,
|
||||
InTradeNo: request.RequestNo, PayChannel: "delivery_admin",
|
||||
OperatorIdentity: account.Identity, OperatorName: account.DisplayName,
|
||||
Ymd: int32(now.Year()*10000 + int(now.Month())*100 + now.Day()),
|
||||
Ym: int32(now.Year()*100 + int(now.Month())),
|
||||
Remark: strings.TrimSpace(request.Reason + " " + request.Remark),
|
||||
}
|
||||
return tx.Create(&record).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, record)
|
||||
}
|
||||
|
||||
func CreateApplyCash(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
wallet, ok := currentWallet(ctx, point)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
WalletBankIdentity string `json:"wallet_bank_identity"`
|
||||
RequestNo string `json:"request_no" binding:"required,max=128"`
|
||||
Amount int64 `json:"amount" binding:"required"`
|
||||
Channel string `json:"channel" binding:"required,max=32"`
|
||||
Remark string `json:"remark" binding:"max=2000"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Amount <= 0 || strings.TrimSpace(request.RequestNo) == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var pending int64
|
||||
if err := db().Model(&models.WalletApplyCash{}).Where("wallet_basic_id = ? AND apply_status = ? AND status <> ?",
|
||||
wallet.ID, common.StatusPending, common.StatusArchived).Select("COALESCE(SUM(amount), 0)").Scan(&pending).Error; err != nil ||
|
||||
request.Amount > wallet.WithdrawalBalance-pending {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var bankID uint64
|
||||
if request.WalletBankIdentity != "" {
|
||||
var bank models.WalletBank
|
||||
if err := common.ActiveRecords(db()).Where("identity = ? AND wallet_basic_id = ?",
|
||||
request.WalletBankIdentity, wallet.ID).First(&bank).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
bankID = bank.ID
|
||||
}
|
||||
apply := models.WalletApplyCash{
|
||||
Entity: common.NewEntity(common.StatusEnable), ApplyStatus: common.StatusPending,
|
||||
WalletBasicID: wallet.ID, WalletBankID: bankID, CashNo: models.NewIdentity(), RequestNo: request.RequestNo,
|
||||
Amount: request.Amount, Channel: request.Channel, Remark: request.Remark,
|
||||
}
|
||||
if err := db().Create(&apply).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, apply)
|
||||
}
|
||||
|
||||
func ListSettlement(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.FinSettlement{}, common.ActiveRecords(db().Model(&models.FinSettlement{})).
|
||||
Where("subject_type = ? AND subject_id = ?", "delivery", point.ID), "fin_settlement.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetSettlement(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var settlement models.FinSettlement
|
||||
respondRecord(ctx, common.ActiveRecords(db()).Where(
|
||||
"identity = ? AND subject_type = ? AND subject_id = ?", ctx.Param("identity"), "delivery", point.ID), &settlement)
|
||||
}
|
||||
50
backend/api/internal/logic/delivery/menu.go
Normal file
50
backend/api/internal/logic/delivery/menu.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package delivery
|
||||
|
||||
import "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
|
||||
type Menu struct {
|
||||
Identity string `json:"identity"`
|
||||
ParentIdentity string `json:"parent_identity,omitempty"`
|
||||
GroupCode string `json:"group_code"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Path string `json:"path"`
|
||||
SortNo int `json:"sort_no"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
var adminMenus = []Menu{
|
||||
{Identity: "dashboard", GroupCode: "dashboard", Name: "数据概述", Icon: "icon-dashboard", Path: "/dashboard", SortNo: 10, Status: common.StatusEnable},
|
||||
{Identity: "dashboard_overview", ParentIdentity: "dashboard", GroupCode: "dashboard", Name: "运营概览", Path: "/dashboard/overview", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "dashboard_reports", ParentIdentity: "dashboard", GroupCode: "dashboard", Name: "订单报表", Path: "/dashboard/reports", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "profile", GroupCode: "profile", Name: "配送点资料", Icon: "icon-storage", Path: "/profile", SortNo: 20, Status: common.StatusEnable},
|
||||
{Identity: "delivery_profile", ParentIdentity: "profile", GroupCode: "profile", Name: "本点资料", Path: "/profile/basic", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "staff", GroupCode: "staff", Name: "配送人员管理", Icon: "icon-user-group", Path: "/staff", SortNo: 30, Status: common.StatusEnable},
|
||||
{Identity: "staff_add", ParentIdentity: "staff", GroupCode: "staff", Name: "新增配送人员", Path: "/staff/add", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "staff_delivery", ParentIdentity: "staff", GroupCode: "staff", Name: "配送人员列表", Path: "/staff/delivery", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "user", GroupCode: "user", Name: "用户管理", Icon: "icon-user", Path: "/user", SortNo: 40, Status: common.StatusEnable},
|
||||
{Identity: "user_account", ParentIdentity: "user", GroupCode: "user", Name: "用户账户", Path: "/user/accounts", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "contract", GroupCode: "contract", Name: "合同管理", Icon: "icon-file", Path: "/contract", SortNo: 50, Status: common.StatusEnable},
|
||||
{Identity: "gasorder_contract", ParentIdentity: "contract", GroupCode: "contract", Name: "配送合同", Path: "/contract/contracts", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "gasorder", GroupCode: "gasorder", Name: "配送订单", Icon: "icon-list", Path: "/gasorder", SortNo: 60, Status: common.StatusEnable},
|
||||
{Identity: "gasorder_create", ParentIdentity: "gasorder", GroupCode: "gasorder", Name: "创建订单", Path: "/gasorder/create", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "gasorder_basic", ParentIdentity: "gasorder", GroupCode: "gasorder", Name: "订单列表", Path: "/gasorder/orders", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "finance", GroupCode: "finance", Name: "财务管理", Icon: "icon-bar-chart", Path: "/finance", SortNo: 70, Status: common.StatusEnable},
|
||||
{Identity: "wallet_basic", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包", Path: "/finance/wallet", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "wallet_bank", ParentIdentity: "finance", GroupCode: "finance", Name: "银行卡", Path: "/finance/banks", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "wallet_payment", ParentIdentity: "finance", GroupCode: "finance", Name: "支付记录", Path: "/finance/payments", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "wallet_record", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包流水", Path: "/finance/records", SortNo: 4, Status: common.StatusEnable},
|
||||
{Identity: "wallet_refund", ParentIdentity: "finance", GroupCode: "finance", Name: "退款记录", Path: "/finance/refunds", SortNo: 5, Status: common.StatusEnable},
|
||||
{Identity: "wallet_recharge", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包充值", Path: "/finance/recharge", SortNo: 6, Status: common.StatusEnable},
|
||||
{Identity: "wallet_apply_cash", ParentIdentity: "finance", GroupCode: "finance", Name: "提现申请", Path: "/finance/withdrawals", SortNo: 7, Status: common.StatusEnable},
|
||||
{Identity: "fin_settlement", ParentIdentity: "finance", GroupCode: "finance", Name: "结算结果", Path: "/finance/settlements", SortNo: 8, Status: common.StatusEnable},
|
||||
{Identity: "invitation", GroupCode: "invitation", Name: "邀请注册", Icon: "icon-qrcode", Path: "/invitation", SortNo: 80, Status: common.StatusEnable},
|
||||
{Identity: "invitation_qrcode", ParentIdentity: "invitation", GroupCode: "invitation", Name: "邀请二维码", Path: "/invitation/qrcode", SortNo: 1, Status: common.StatusEnable},
|
||||
}
|
||||
|
||||
func MenusForRole(roleCode string) []Menu {
|
||||
if roleCode != "admin" {
|
||||
return nil
|
||||
}
|
||||
return append([]Menu(nil), adminMenus...)
|
||||
}
|
||||
23
backend/api/internal/logic/delivery/menu_test.go
Normal file
23
backend/api/internal/logic/delivery/menu_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package delivery
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMenusOnlyAllowAdmin(t *testing.T) {
|
||||
if len(MenusForRole("admin")) == 0 {
|
||||
t.Fatal("admin must receive delivery menus")
|
||||
}
|
||||
for _, role := range []string{"root", "dispatcher", "warehouse", "quality", "delivery", ""} {
|
||||
if len(MenusForRole(role)) != 0 {
|
||||
t.Fatalf("role %q must not receive delivery admin menus", role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMenusExcludeInventoryAndEcommerce(t *testing.T) {
|
||||
for _, menu := range MenusForRole("admin") {
|
||||
switch menu.GroupCode {
|
||||
case "inventory", "warehouse", "ec":
|
||||
t.Fatalf("forbidden menu group %q", menu.GroupCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
406
backend/api/internal/logic/delivery/order.go
Normal file
406
backend/api/internal/logic/delivery/order.go
Normal file
@@ -0,0 +1,406 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"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
|
||||
}
|
||||
|
||||
func ListContract(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.GasorderContract{}, common.ActiveRecords(db().Model(&models.GasorderContract{})).
|
||||
Where("gas_basic_id = ? AND delivery_basic_id = ?", point.GasBasicID, point.ID), "gasorder_contract.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetContract(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
if _, valid := scopedContract(ctx, ctx.Param("identity"), point.ID); valid {
|
||||
platformgasorder.GetGasorderContract(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)
|
||||
}
|
||||
|
||||
func ListContractProduct(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(db().Model(&models.GasorderContractProduct{})).
|
||||
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_product.gasorder_contract_id").
|
||||
Where("gasorder_contract.delivery_basic_id = ?", point.ID)
|
||||
listScoped(ctx, &models.GasorderContractProduct{}, query, "gasorder_contract_product.created_at desc")
|
||||
}
|
||||
|
||||
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 wallet_payment ON wallet_payment.id = gasorder_payment.wallet_payment_id").
|
||||
Where("gasorder_payment.gasorder_basic_id = ? AND wallet_payment.payment_status = ?", order.ID, common.StatusPaid).
|
||||
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})
|
||||
}
|
||||
33
backend/api/internal/logic/delivery/resource_contract.go
Normal file
33
backend/api/internal/logic/delivery/resource_contract.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package delivery
|
||||
|
||||
type ResourceContract struct {
|
||||
Domain string `json:"domain"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
PageKind string `json:"pageKind"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
func ExpectedResources() []ResourceContract {
|
||||
items := []ResourceContract{
|
||||
{"profile", "delivery_profile", "/delivery_profile", "list", "readonly"},
|
||||
{"staff", "staff_account", "/staff_account", "list", "writable"},
|
||||
{"staff", "staff_credential", "/staff_credential", "list", "writable"},
|
||||
{"user", "user_account", "/user_account", "list", "writable"},
|
||||
{"user", "user_address", "/user_address", "list", "writable"},
|
||||
{"contract", "gasorder_contract", "/gasorder_contract", "list", "managed"},
|
||||
{"contract", "gasorder_contract_product", "/gasorder_contract_product", "list", "append_only"},
|
||||
{"contract", "gasorder_contract_revision", "/gasorder_contract_revision", "list", "readonly"},
|
||||
{"contract", "product_info", "/product_info", "list", "readonly"},
|
||||
{"gasorder", "gasorder_basic", "/gasorder_basic", "list", "append_only"},
|
||||
{"finance", "wallet_basic", "/wallet_basic", "list", "readonly"},
|
||||
{"finance", "wallet_bank", "/wallet_bank", "list", "readonly"},
|
||||
{"finance", "wallet_payment", "/wallet_payment", "list", "readonly"},
|
||||
{"finance", "wallet_record", "/wallet_record", "list", "readonly"},
|
||||
{"finance", "wallet_refund", "/wallet_refund", "list", "readonly"},
|
||||
{"finance", "wallet_recharge", "/wallet_recharge", "list", "append_only"},
|
||||
{"finance", "wallet_apply_cash", "/wallet_apply_cash", "list", "append_only"},
|
||||
{"finance", "fin_settlement", "/fin_settlement", "list", "readonly"},
|
||||
}
|
||||
return items
|
||||
}
|
||||
271
backend/api/internal/logic/delivery/staff.go
Normal file
271
backend/api/internal/logic/delivery/staff.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"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"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func scopedStaff(ctx *gin.Context, identity string, point models.DeliveryBasic) (models.StaffAccount, bool) {
|
||||
var staff models.StaffAccount
|
||||
if err := common.ActiveRecords(db()).Where("identity = ? AND gas_basic_id = ? AND delivery_basic_id = ?",
|
||||
identity, point.GasBasicID, point.ID).First(&staff).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return staff, false
|
||||
}
|
||||
return staff, true
|
||||
}
|
||||
|
||||
func ListStaff(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(db().Model(&models.StaffAccount{})).
|
||||
Where("gas_basic_id = ? AND delivery_basic_id = ? AND role_code = ?", point.GasBasicID, point.ID, "delivery")
|
||||
listScoped(ctx, &models.StaffAccount{}, query, "staff_account.created_at desc")
|
||||
}
|
||||
|
||||
func GetStaff(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var staff models.StaffAccount
|
||||
respondRecord(ctx, common.ActiveRecords(db()).Where("identity = ? AND gas_basic_id = ? AND delivery_basic_id = ? AND role_code = ?",
|
||||
ctx.Param("identity"), point.GasBasicID, point.ID, "delivery"), &staff)
|
||||
}
|
||||
|
||||
type staffRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
WorkStatus string `json:"work_status" binding:"required"`
|
||||
}
|
||||
|
||||
func CreateStaff(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request staffRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Username == "" ||
|
||||
!common.IsValidAccountPassword(request.Password) || (request.WorkStatus != "on_duty" && request.WorkStatus != "off_duty") {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := common.PasswordHash(request.Password)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
staff := models.StaffAccount{
|
||||
Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash,
|
||||
Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: "delivery",
|
||||
GasBasicID: point.GasBasicID, DeliveryBasicID: point.ID, WorkStatus: request.WorkStatus,
|
||||
}
|
||||
if err := db().Create(&staff).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, staff)
|
||||
}
|
||||
|
||||
func UpdateStaff(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := scopedStaff(ctx, ctx.Param("identity"), point); !ok {
|
||||
return
|
||||
}
|
||||
var request staffRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil ||
|
||||
(request.WorkStatus != "on_duty" && request.WorkStatus != "off_duty") {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.UpdateAllowedByIdentity(ctx, &models.StaffAccount{}, gin.H{
|
||||
"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "work_status": request.WorkStatus,
|
||||
}, []string{"name", "phone", "avatar", "work_status"})
|
||||
}
|
||||
|
||||
func ResetStaffPassword(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
staff, ok := scopedStaff(ctx, ctx.Param("identity"), point)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsValidAccountPassword(request.Password) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, _ := common.PasswordHash(request.Password)
|
||||
if err := db().Model(&staff).Update("password_hash", hash).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"changed": true})
|
||||
}
|
||||
|
||||
func UpdateStaffStatus(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := scopedStaff(ctx, ctx.Param("identity"), point); ok {
|
||||
common.UpdateRecordStatus(ctx, &models.StaffAccount{})
|
||||
}
|
||||
}
|
||||
|
||||
func ArchiveStaff(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
staff, ok := scopedStaff(ctx, ctx.Param("identity"), point)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := db().Model(&models.GasorderBasic{}).
|
||||
Where("staff_account_id = ? AND order_status NOT IN ?", staff.ID, []int{common.StatusCompleted, common.StatusCancelled}).
|
||||
Count(&count).Error; err != nil || count > 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := db().Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&models.StaffCredential{}).Where("staff_account_id = ?", staff.ID).
|
||||
Update("status", common.StatusArchived).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&staff).Update("status", common.StatusArchived).Error
|
||||
}); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"archived": true})
|
||||
}
|
||||
|
||||
func credentialQuery(point models.DeliveryBasic) *gorm.DB {
|
||||
return common.ActiveRecords(db().Model(&models.StaffCredential{})).
|
||||
Joins("JOIN staff_account ON staff_account.id = staff_credential.staff_account_id").
|
||||
Where("staff_account.delivery_basic_id = ? AND staff_account.gas_basic_id = ? AND staff_account.role_code = ?",
|
||||
point.ID, point.GasBasicID, "delivery")
|
||||
}
|
||||
|
||||
func ListCredential(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.StaffCredential{}, credentialQuery(point), "staff_credential.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetCredential(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var credential models.StaffCredential
|
||||
respondRecord(ctx, credentialQuery(point).Where("staff_credential.identity = ?", ctx.Param("identity")), &credential)
|
||||
}
|
||||
|
||||
type credentialRequest struct {
|
||||
StaffIdentity string `json:"staff_account_identity" binding:"required"`
|
||||
CredentialType string `json:"credential_type" binding:"required,max=64"`
|
||||
CredentialNo string `json:"credential_no" binding:"max=128"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
}
|
||||
|
||||
func CreateCredential(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request credentialRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staff, ok := scopedStaff(ctx, request.StaffIdentity, point)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
item := models.StaffCredential{
|
||||
Entity: common.NewEntity(common.StatusEnable), StaffAccountID: staff.ID,
|
||||
CredentialType: request.CredentialType, CredentialNo: request.CredentialNo, ExpiredAt: request.ExpiredAt,
|
||||
}
|
||||
if err := db().Create(&item).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, item)
|
||||
}
|
||||
|
||||
func UpdateCredential(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var existing models.StaffCredential
|
||||
if err := credentialQuery(point).Where("staff_credential.identity = ?", ctx.Param("identity")).First(&existing).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var request credentialRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staff, ok := scopedStaff(ctx, request.StaffIdentity, point)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := db().Model(&existing).Updates(map[string]any{
|
||||
"staff_account_id": staff.ID, "credential_type": request.CredentialType,
|
||||
"credential_no": request.CredentialNo, "expired_at": request.ExpiredAt,
|
||||
}).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func UpdateCredentialStatus(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := credentialQuery(point).Where("staff_credential.identity = ?", ctx.Param("identity")).Count(&count).Error; err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.UpdateRecordStatus(ctx, &models.StaffCredential{})
|
||||
}
|
||||
|
||||
func ArchiveCredential(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := credentialQuery(point).Where("staff_credential.identity = ?", ctx.Param("identity")).Count(&count).Error; err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.ArchiveRecord(ctx, &models.StaffCredential{})
|
||||
}
|
||||
328
backend/api/internal/logic/delivery/user.go
Normal file
328
backend/api/internal/logic/delivery/user.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"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"
|
||||
)
|
||||
|
||||
func userQuery(pointID uint64) *gorm.DB {
|
||||
return common.ActiveRecords(db().Model(&models.UserAccount{})).
|
||||
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_account.id AND user_service_relation.status <> ?",
|
||||
common.StatusArchived).
|
||||
Where("user_service_relation.delivery_basic_id = ?", pointID)
|
||||
}
|
||||
|
||||
func scopedUser(ctx *gin.Context, identity string, pointID uint64) (models.UserAccount, models.UserServiceRelation, bool) {
|
||||
var user models.UserAccount
|
||||
if err := userQuery(pointID).Where("user_account.identity = ?", identity).First(&user).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return user, models.UserServiceRelation{}, false
|
||||
}
|
||||
var relation models.UserServiceRelation
|
||||
if err := common.ActiveRecords(db()).Where("user_account_id = ? AND delivery_basic_id = ?", user.ID, pointID).First(&relation).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return user, relation, false
|
||||
}
|
||||
return user, relation, true
|
||||
}
|
||||
|
||||
func ListUser(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.UserAccount{}, userQuery(point.ID), "user_account.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetUser(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, relation, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var addresses []models.UserAddress
|
||||
if err := common.ActiveRecords(db()).Where("user_account_id = ?", user.ID).Order("is_default desc, created_at desc").Find(&addresses).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, _ := common.PublicResourceResponse(gin.H{"user": user, "relation": relation, "addresses": addresses})
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
type userRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
RealName string `json:"real_name" binding:"max=64"`
|
||||
}
|
||||
|
||||
func CreateUser(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request userRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Username == "" || !common.IsValidAccountPassword(request.Password) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := common.PasswordHash(request.Password)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
user := models.UserAccount{
|
||||
Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash,
|
||||
Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RealName: request.RealName,
|
||||
}
|
||||
relation := models.UserServiceRelation{
|
||||
Entity: common.NewEntity(common.StatusEnable), GasBasicID: point.GasBasicID, DeliveryBasicID: point.ID,
|
||||
}
|
||||
if err := db().Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
relation.UserAccountID = user.ID
|
||||
return tx.Create(&relation).Error
|
||||
}); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, user)
|
||||
}
|
||||
|
||||
func UpdateUser(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, _, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request userRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := db().Model(&user).Updates(map[string]any{
|
||||
"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "real_name": request.RealName,
|
||||
}).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func ResetUserPassword(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, _, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsValidAccountPassword(request.Password) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, _ := common.PasswordHash(request.Password)
|
||||
if err := db().Model(&user).Update("password_hash", hash).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"changed": true})
|
||||
}
|
||||
|
||||
func UpdateUserStatus(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, _, ok := scopedUser(ctx, ctx.Param("identity"), point.ID); ok {
|
||||
common.UpdateRecordStatus(ctx, &models.UserAccount{})
|
||||
}
|
||||
}
|
||||
|
||||
func ArchiveUser(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, relation, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var blocking int64
|
||||
if err := db().Model(&models.GasorderBasic{}).Where("user_account_id = ? AND order_status NOT IN ?",
|
||||
user.ID, []int{common.StatusCompleted, common.StatusCancelled}).Count(&blocking).Error; err != nil || blocking > 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := db().Model(&models.CsTicket{}).Where("user_account_id = ? AND status <> ? AND ticket_status = ?",
|
||||
user.ID, common.StatusArchived, common.StatusOpen).Count(&blocking).Error; err != nil || blocking > 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
err := db().Where("owner_type = ? AND owner_id = ? AND status <> ?", "user", user.ID, common.StatusArchived).First(&wallet).Error
|
||||
if err == nil && (wallet.Balance > 0 || wallet.WithdrawalBalance > 0) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
if cashErr := db().Model(&models.WalletApplyCash{}).Where("wallet_basic_id = ? AND apply_status = ? AND status <> ?",
|
||||
wallet.ID, common.StatusPending, common.StatusArchived).Count(&blocking).Error; cashErr != nil || blocking > 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := db().Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&relation).Update("status", common.StatusArchived).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&models.UserAddress{}).Where("user_account_id = ?", user.ID).Update("status", common.StatusArchived).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&user).Update("status", common.StatusArchived).Error
|
||||
}); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"archived": true})
|
||||
}
|
||||
|
||||
func addressQuery(pointID uint64) *gorm.DB {
|
||||
return common.ActiveRecords(db().Model(&models.UserAddress{})).
|
||||
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_address.user_account_id AND user_service_relation.status <> ?",
|
||||
common.StatusArchived).Where("user_service_relation.delivery_basic_id = ?", pointID)
|
||||
}
|
||||
|
||||
func ListAddress(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.UserAddress{}, addressQuery(point.ID), "user_address.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetAddress(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var address models.UserAddress
|
||||
respondRecord(ctx, addressQuery(point.ID).Where("user_address.identity = ?", ctx.Param("identity")), &address)
|
||||
}
|
||||
|
||||
type addressRequest struct {
|
||||
UserIdentity string `json:"user_account_identity" binding:"required"`
|
||||
Address string `json:"address" binding:"required,max=255"`
|
||||
Longitude string `json:"longitude" binding:"max=32"`
|
||||
Latitude string `json:"latitude" binding:"max=32"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
func CreateAddress(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request addressRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
user, _, ok := scopedUser(ctx, request.UserIdentity, point.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
address := models.UserAddress{
|
||||
Entity: common.NewEntity(common.StatusEnable), UserAccountID: user.ID, Address: request.Address,
|
||||
Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault,
|
||||
}
|
||||
if err := db().Transaction(func(tx *gorm.DB) error {
|
||||
if request.IsDefault {
|
||||
if err := tx.Model(&models.UserAddress{}).Where("user_account_id = ?", user.ID).Update("is_default", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Create(&address).Error
|
||||
}); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, address)
|
||||
}
|
||||
|
||||
func UpdateAddress(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var existing models.UserAddress
|
||||
if err := addressQuery(point.ID).Where("user_address.identity = ?", ctx.Param("identity")).First(&existing).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var request addressRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
user, _, ok := scopedUser(ctx, request.UserIdentity, point.ID)
|
||||
if !ok || user.ID != existing.UserAccountID {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := db().Transaction(func(tx *gorm.DB) error {
|
||||
if request.IsDefault {
|
||||
if err := tx.Model(&models.UserAddress{}).Where("user_account_id = ? AND id <> ?", user.ID, existing.ID).
|
||||
Update("is_default", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Model(&existing).Updates(map[string]any{
|
||||
"address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude, "is_default": request.IsDefault,
|
||||
}).Error
|
||||
}); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func UpdateAddressStatus(ctx *gin.Context) { mutateAddress(ctx, false) }
|
||||
func ArchiveAddress(ctx *gin.Context) { mutateAddress(ctx, true) }
|
||||
func mutateAddress(ctx *gin.Context, archive bool) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := addressQuery(point.ID).Where("user_address.identity = ?", ctx.Param("identity")).Count(&count).Error; err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if archive {
|
||||
common.ArchiveRecord(ctx, &models.UserAddress{})
|
||||
} else {
|
||||
common.UpdateRecordStatus(ctx, &models.UserAddress{})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user