已完成用户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,98 @@
// 功能描述:验证地址坐标、所有权、事务回滚及创建幂等,不访问远程数据。
// 版本1.0.0。
package user
import (
"git.apinb.com/bsm-sdk/core/types"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
"net/http/httptest"
"strings"
"testing"
)
func TestAddressCoordinates(t *testing.T) {
for _, tc := range []struct {
lon, lat string
valid bool
}{
{"", "", true}, {"113.2", "23.4", true}, {"180", "-90", true}, {"181", "0", false},
{"0", "91", false}, {"0", "", false}, {"NaN", "0", false}, {"Inf", "0", false},
} {
if validAddressCoordinates(tc.lon, tc.lat) != tc.valid {
t.Errorf("坐标边界不正确:%q %q", tc.lon, tc.lat)
}
}
}
// addressContext 构造当前用户鉴权,不依赖登录接口。
func addressContext(body, identity string) (*gin.Context, *httptest.ResponseRecorder) {
response := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(response)
ctx.Set("Auth", &types.JwtClaims{Client: "user_app", Identity: "alice"})
ctx.Request = httptest.NewRequest("POST", "/addresses", strings.NewReader(body))
ctx.Request.Header.Set("Content-Type", "application/json")
ctx.Params = gin.Params{{Key: "identity", Value: identity}}
return ctx, response
}
func expectAddressAccount(mock sqlmock.Sqlmock) {
mock.ExpectQuery(`SELECT .* FROM "user_account"`).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "name", "phone"}).AddRow(1, "alice", "测试用户", "13800000001"))
}
func expectAddressLock(mock sqlmock.Sqlmock) {
mock.ExpectBegin()
mock.ExpectQuery(`SELECT "id" FROM "user_account".*FOR UPDATE`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
}
func TestAddressMutationsRejectOtherOwner(t *testing.T) {
for _, action := range []func(*gin.Context){SetDefaultAddress, DeleteAddress, UpdateAddress} {
mock := primaryTestDB(t)
expectAddressAccount(mock)
expectAddressLock(mock)
mock.ExpectQuery(`SELECT .* FROM "user_address".*identity = \$1 AND user_account_id = \$2`).WillReturnRows(sqlmock.NewRows([]string{"id"}))
mock.ExpectRollback()
ctx, response := addressContext(`{"address":"测试地址"}`, "other-address")
action(ctx)
if strings.Contains(response.Body.String(), `"code":0`) {
t.Fatal("越权修改成功")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
}
func TestAddressCreateRetryDoesNotDuplicateOrResetDefault(t *testing.T) {
mock := primaryTestDB(t)
expectAddressAccount(mock)
expectAddressLock(mock)
mock.ExpectQuery(`SELECT .* FROM "user_address".*request_no`).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "address", "contact_name", "contact_phone"}).AddRow(5, "existing", 1, "测试地址", "测试用户", "13800000001"))
mock.ExpectCommit()
ctx, response := addressContext(`{"address":"测试地址","request_no":"same-request","is_default":true}`, "")
SaveAddress(ctx)
if !strings.Contains(response.Body.String(), `"identity":"existing"`) {
t.Fatal(response.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestAddressDefaultUpdateFailureRollsBack(t *testing.T) {
mock := primaryTestDB(t)
expectAddressAccount(mock)
expectAddressLock(mock)
mock.ExpectQuery(`SELECT .* FROM "user_address"`).WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status"}).AddRow(5, "owned", 1))
mock.ExpectExec(`UPDATE "user_address" SET "is_default"`).WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE "user_address" SET "is_default"`).WillReturnError(sqlmock.ErrCancelled)
mock.ExpectRollback()
ctx, response := addressContext("", "owned")
SetDefaultAddress(ctx)
if strings.Contains(response.Body.String(), `"code":0`) {
t.Fatal("失败写入被当作成功")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}