已完成用户APP首期功能开发

交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
czl231
2026-09-13 00:57:32 +08:00
parent a0a4bb1218
commit 3ef33b531d
793 changed files with 40959 additions and 1204 deletions

View File

@@ -0,0 +1,47 @@
// 功能描述:供气单位答复本人合同变更申请,不自动改动合同条款;版本:1.0.0。
package gas
import (
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"strings"
)
// ResolveContractRequest 按原合同气站锁定申请,提交处理结果后等待用户确认。
func ResolveContractRequest(ctx *gin.Context) {
account, station, ok := CurrentGasAccount(ctx)
if !ok {
return
}
var request struct {
Result string `json:"result" binding:"required,max=2000"`
}
if ctx.ShouldBindJSON(&request) != nil || strings.TrimSpace(request.Result) == "" {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
result := strings.TrimSpace(request.Result)
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
var ticket models.CsTicket
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND gas_basic_id = ? AND gasorder_contract_id <> 0 AND category = ? AND status <> ?", ctx.Param("identity"), station.ID, "contract_change", 3).First(&ticket).Error; err != nil {
return err
}
if (ticket.TicketStatus == 34 || ticket.TicketStatus == 23) && ticket.Result == result {
return nil
}
if ticket.TicketStatus != 32 && ticket.TicketStatus != 11 {
return errcode.ErrInvalidArgument
}
return tx.Model(&ticket).Updates(map[string]any{"result": result, "ticket_status": 34, "operator_identity": account.Identity}).Error
})
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"updated": true})
}

View File

@@ -0,0 +1,71 @@
// 功能描述:合同申请答复的原气站归属、状态锁与幂等;版本:1.0.0。
package gas
import (
"git.apinb.com/bsm-sdk/core/types"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"net/http/httptest"
"strings"
"testing"
)
func TestResolveContractRequestScopeAndState(t *testing.T) {
for _, tc := range []struct {
name string
state int
result string
found, success, update bool
}{
{"待受理答复", 32, "", true, true, true}, {"处理中答复", 11, "", true, true, true},
{"重复答复", 34, "已处理", true, true, false}, {"不得覆盖答复", 34, "原答复", true, false, false},
{"取消后拒绝", 22, "", true, false, false}, {"其他气站", 0, "", false, false, false},
} {
t.Run(tc.name, func(t *testing.T) {
connection, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer connection.Close()
db, err := gorm.Open(postgres.New(postgres.Config{Conn: connection}), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
previous := impl.DBService
impl.DBService = db
defer func() { impl.DBService = previous }()
mock.ExpectQuery(`SELECT .* FROM "gas_account"`).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "gas_basic_id"}).AddRow(2, "gas", 9))
mock.ExpectQuery(`SELECT .* FROM "gas_basic"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(9))
mock.ExpectBegin()
rows := sqlmock.NewRows([]string{"id", "ticket_status", "result"})
if tc.found {
rows.AddRow(7, tc.state, tc.result)
}
mock.ExpectQuery(`SELECT .* FROM "cs_ticket".*identity = \$1 AND gas_basic_id = \$2 AND gasorder_contract_id <> 0 AND category = \$3 AND status <> \$4.*FOR UPDATE`).WithArgs("request", 9, "contract_change", 3, 1).WillReturnRows(rows)
if tc.update {
mock.ExpectExec(`UPDATE "cs_ticket" SET .*"result"=.*"ticket_status"=`).WillReturnResult(sqlmock.NewResult(0, 1))
}
if tc.success {
mock.ExpectCommit()
} else {
mock.ExpectRollback()
}
response := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(response)
ctx.Set("Auth", &types.JwtClaims{Client: "gas_admin", Identity: "gas"})
ctx.Params = gin.Params{{Key: "identity", Value: "request"}}
ctx.Request = httptest.NewRequest("POST", "/requests", strings.NewReader(`{"result":"已处理"}`))
ctx.Request.Header.Set("Content-Type", "application/json")
ResolveContractRequest(ctx)
if strings.Contains(response.Body.String(), `"code":0`) != tc.success {
t.Fatal(response.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
})
}
}

View File

@@ -19,12 +19,13 @@ func ticketQueryWithDB(databaseService *gorm.DB, gasID uint64) *gorm.DB {
return common.ActiveRecords(databaseService.Model(&models.CsTicket{})).
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = cs_ticket.user_account_id AND user_service_relation.status <> ?",
common.StatusArchived).
Where("user_service_relation.gas_basic_id = ?", gasID)
Where("((cs_ticket.gasorder_contract_id = 0 AND user_service_relation.gas_basic_id = ?) OR (cs_ticket.gasorder_contract_id <> 0 AND cs_ticket.gas_basic_id = ?))", gasID, gasID)
}
// ticketDetailDisplay 为工单详情附加当前记录所引用关系的可读名称,不修改工单归属数据。
type ticketDetailDisplay struct {
models.CsTicket
ContractRequestOpen int `gorm:"column:contract_request_open" json:"contract_request_open"`
UserAccountDisplayName string `gorm:"column:user_account_display_name" json:"user_account_display_name"`
DeliveryBasicDisplayName string `gorm:"column:delivery_basic_display_name" json:"delivery_basic_display_name"`
StaffAccountDisplayName string `gorm:"column:staff_account_display_name" json:"staff_account_display_name"`
@@ -40,6 +41,7 @@ func ticketDetailQuery(gasID uint64) *gorm.DB {
func ticketDetailQueryWithDB(databaseService *gorm.DB, gasID uint64) *gorm.DB {
return ticketQueryWithDB(databaseService, gasID).
Select(`cs_ticket.*,
CASE WHEN cs_ticket.gasorder_contract_id <> 0 AND cs_ticket.category = 'contract_change' AND cs_ticket.ticket_status IN (32, 11) THEN 1 ELSE 0 END AS contract_request_open,
COALESCE(NULLIF(ticket_user.name, ''), NULLIF(ticket_user.real_name, ''), NULLIF(ticket_user.username, ''), '用户记录已失效') AS user_account_display_name,
CASE WHEN cs_ticket.delivery_basic_id = 0 THEN '' ELSE COALESCE(NULLIF(ticket_delivery.name, ''), '配送点记录已失效') END AS delivery_basic_display_name,
CASE WHEN cs_ticket.staff_account_id = 0 THEN '' ELSE COALESCE(NULLIF(ticket_staff.name, ''), NULLIF(ticket_staff.username, ''), '工作人员记录已失效') END AS staff_account_display_name,
@@ -122,6 +124,10 @@ func UpdateTicket(ctx *gin.Context) {
if !ok {
return
}
if ticket.GasorderContractID != 0 && (user.ID != ticket.UserAccountID || request.Category != "contract_change") {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err := impl.DBService.Model(&ticket).Updates(map[string]any{
"user_account_id": user.ID, "ticket_no": request.TicketNo, "category": request.Category, "priority": request.Priority,
}).Error; err != nil {

View File

@@ -5,6 +5,7 @@ package gas
var supportedTicketCategories = map[string]struct{}{
"delivery": {}, "installation": {}, "repair": {},
"inspection": {}, "reinspection": {}, "customer_service": {},
"contract_change": {},
}
var supportedTicketPriorities = map[string]struct{}{

View File

@@ -44,6 +44,9 @@ func TestTicketDetailQueryIncludesScopedRelationNames(t *testing.T) {
Find(&ticketDetailDisplay{}).Statement.SQL.String()
for _, fragment := range []string{
"user_service_relation.gas_basic_id =",
"cs_ticket.gasorder_contract_id = 0",
"cs_ticket.gasorder_contract_id <> 0 AND cs_ticket.gas_basic_id =",
"contract_request_open",
"LEFT JOIN user_account AS ticket_user",
"LEFT JOIN delivery_basic AS ticket_delivery",
"LEFT JOIN staff_account AS ticket_staff",