确定友商登陆逻辑
This commit is contained in:
parent
17bc00a42f
commit
a2de599f81
79
doc/partner.md
Normal file
79
doc/partner.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# 金蚕游戏管理后台相关说明
|
||||||
|
|
||||||
|
## 修改记录
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
1. 所有请求参数中带*号的不能为空
|
||||||
|
2. 如无特殊说明, 所有接口返回json, 顶级结构如下, 接口Response的数据结构说明只包含data部分
|
||||||
|
|
||||||
|
``` JSON
|
||||||
|
{
|
||||||
|
"code": 0, //0:成功 2: 缺少必要参数(accountid, sessionid) 4: 帐号被封, 5: 帐号未找到 100: 所有未定义的错误
|
||||||
|
"msg": "", //错误描述, 一般在code=0时, 该字段为空
|
||||||
|
"data": {}, // 数据
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 页面列表
|
||||||
|
|
||||||
|
> 所有的页面均可单独调用, url: https://puzzle-admin.kingsome.cn/页面url?token=token&mini=1
|
||||||
|
> token为 1号接口获取
|
||||||
|
|
||||||
|
| 页面名称 | 页面url |
|
||||||
|
| ---------- | ----------------- |
|
||||||
|
| 店铺管理员 | /#/shop/shopadmin |
|
||||||
|
| 游戏设置 | /#/shop/setting |
|
||||||
|
| 活动列表 | /#/shop/activity_list |
|
||||||
|
| 挑战活动列表 | /#/shop/exam_list |
|
||||||
|
| 抽奖转盘 | /#/shop/lottery_setting |
|
||||||
|
| 分享设置 | /#/shop/share_setting |
|
||||||
|
| 优惠券设置 | /#/marketing/coupon |
|
||||||
|
| 题库设置 | /#/question/setting |
|
||||||
|
| 自定义题库 | /#/question/shoppuzzles |
|
||||||
|
| 邮件 | 暂不开放 |
|
||||||
|
| 公告 | 暂不开放 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 接口列表
|
||||||
|
|
||||||
|
### 1. 获取token
|
||||||
|
|
||||||
|
1. Method: POST
|
||||||
|
2. URI: /api/partner/login
|
||||||
|
|
||||||
|
> POST参数
|
||||||
|
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| -------- | -------------------------------------- |
|
||||||
|
| name | *店铺完整名称 |
|
||||||
|
| sname | 店铺显示的短名称(如果该字段为空的话, 取name字段) |
|
||||||
|
| sid | *唯一的店铺id, 接口根据此字段判断该次上报是否是同一家店铺 |
|
||||||
|
| logo | 店铺的logo, 一个可以外网访问的url地址 |
|
||||||
|
| timestamp | *10或13位均可 |
|
||||||
|
| sign | *签名 |
|
||||||
|
|
||||||
|
> 签名字段说明:
|
||||||
|
>
|
||||||
|
> 取name,sid, timestamp和我们提供的SecretKey字段拼接成 name=店铺名称&sid=店铺id:timestamp:SecretKey, 取该字符串的sha1
|
||||||
|
|
||||||
|
```js
|
||||||
|
let signStr = `name=${name}&sid=${sid}:${timestamp}:${secretKey}`
|
||||||
|
let sha1sum = crypto.createHash('sha1')
|
||||||
|
sha1sum.update(signStr)
|
||||||
|
let sign = sha1sum.digest('hex')
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
3. Response: JSON
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
token: '1231231aasa'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -2,11 +2,12 @@ import BaseController from '../../common/base.controller'
|
|||||||
import { permission, role, router } from '../../decorators/router'
|
import { permission, role, router } from '../../decorators/router'
|
||||||
import { ZError } from '../../common/ZError'
|
import { ZError } from '../../common/ZError'
|
||||||
import { Game } from '../../models/content/Game'
|
import { Game } from '../../models/content/Game'
|
||||||
import { generateQrFile } from '../../services/File'
|
import { downloadRemoteFile, generateQrFile } from '../../services/File'
|
||||||
import { getInviteeNum } from '../../services/JCFW'
|
import { getInviteeNum } from '../../services/JCFW'
|
||||||
import { UserItem } from '../../models/user/UserItem'
|
import { UserItem } from '../../models/user/UserItem'
|
||||||
import { checkText } from '../../services/Baidu'
|
import { checkText } from '../../services/Baidu'
|
||||||
import { msgSecCheck, refreshToken } from '../../services/Wechat'
|
import { msgSecCheck, refreshToken } from '../../services/Wechat'
|
||||||
|
import { uploadToCDN } from '../../services/TencentCDN'
|
||||||
|
|
||||||
class GameController extends BaseController {
|
class GameController extends BaseController {
|
||||||
@role('anon')
|
@role('anon')
|
||||||
@ -27,8 +28,12 @@ class GameController extends BaseController {
|
|||||||
// // return { token }
|
// // return { token }
|
||||||
// const { data } = await msgSecCheck(txt)
|
// const { data } = await msgSecCheck(txt)
|
||||||
// return { baidu: res.data, wx: data }
|
// return { baidu: res.data, wx: data }
|
||||||
return {}
|
let { url } = req.params
|
||||||
|
let path = (await downloadRemoteFile(url)) as string
|
||||||
|
let { data } = await uploadToCDN(path, 'test')
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
@permission(['game:read', 'shop:game_setting'])
|
@permission(['game:read', 'shop:game_setting'])
|
||||||
@router('post /api/games')
|
@router('post /api/games')
|
||||||
async list(req, res) {
|
async list(req, res) {
|
||||||
|
70
src/admin/controllers/partner.controller.ts
Normal file
70
src/admin/controllers/partner.controller.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import BaseController from '../../common/base.controller'
|
||||||
|
import { router } from '../../decorators/router'
|
||||||
|
import { customAlphabet } from 'nanoid'
|
||||||
|
import { ZError } from '../../common/ZError'
|
||||||
|
import { checkSign, createSign } from '../../utils/security.util'
|
||||||
|
import { Shop } from '../../models/shop/Shop'
|
||||||
|
import { Admin } from '../../models/admin/Admin'
|
||||||
|
import { Game } from '../../models/content/Game'
|
||||||
|
import { downloadRemoteFile } from '../../services/File'
|
||||||
|
import { uploadToCDN } from '../../services/TencentCDN'
|
||||||
|
|
||||||
|
const SECRET_KEY = '37284c327e10d8b73cf4325f33a3de4b34032e3e'
|
||||||
|
const passwd = customAlphabet('2345678abcdefghjkmnpqrstwxy', 10)
|
||||||
|
|
||||||
|
class PartnerController extends BaseController {
|
||||||
|
/**
|
||||||
|
* 店铺名
|
||||||
|
* 店铺短名字
|
||||||
|
* logo?
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @return {Promise<{}>}
|
||||||
|
*/
|
||||||
|
@router('post /api/partner/login')
|
||||||
|
async login(req: any, res: any) {
|
||||||
|
let { name, sname, sid, logo, timestamp, sign } = req.params
|
||||||
|
if (!name || !sid || !timestamp || !sign) {
|
||||||
|
throw new ZError(10, '缺少必要参数')
|
||||||
|
}
|
||||||
|
const signKeys = ['name', 'sid']
|
||||||
|
if (!checkSign({ secretKey: SECRET_KEY, data: req.params, timestamp, sign, signKeys })) {
|
||||||
|
throw new ZError(21, 'sign error')
|
||||||
|
}
|
||||||
|
sname = sname || name
|
||||||
|
const shopData = { name, showName: sname, source: 1, publish: true }
|
||||||
|
const shop = await Shop.insertOrUpdate({ partnerId: sid }, shopData)
|
||||||
|
// 保存一下, 以生成sid和numid
|
||||||
|
if (shop.newRecord) {
|
||||||
|
let game = await Game.findAvaOne()
|
||||||
|
if (game) {
|
||||||
|
shop.gameInfo = { gameid: game.id, versionid: game.versions[0]._id + '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shop.logoPartner !== logo) {
|
||||||
|
shop.logoPartner = logo
|
||||||
|
setImmediate(async () => {
|
||||||
|
let path = (await downloadRemoteFile(logo)) as string
|
||||||
|
let { data } = await uploadToCDN(path, 'shop')
|
||||||
|
console.log(`upload logo finished`, data)
|
||||||
|
if (!data.errcode) {
|
||||||
|
shop.logo = data.url_cdn
|
||||||
|
await shop.save()
|
||||||
|
account.avatar = data.url_cdn
|
||||||
|
await account.save()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await shop.save()
|
||||||
|
|
||||||
|
const username = sid
|
||||||
|
let accountData = { level: 9, roles: ['shopadmin'], department: shop.id, showname: sname }
|
||||||
|
const account = await Admin.insertOrUpdate({ username }, accountData)
|
||||||
|
if (account.newRecord) {
|
||||||
|
account.updatePassword(shop.sid)
|
||||||
|
await account.save()
|
||||||
|
}
|
||||||
|
const token = await res.jwtSign({ id: account.id })
|
||||||
|
return { token }
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { getModelForClass, index, modelOptions, prop, ReturnModelType } from '@typegoose/typegoose'
|
import { getModelForClass, index, modelOptions, pre, prop, ReturnModelType } from '@typegoose/typegoose'
|
||||||
import { dbconn } from 'decorators/dbconn'
|
import { dbconn } from 'decorators/dbconn'
|
||||||
|
|
||||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
|
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
|
||||||
@ -35,6 +35,9 @@ export interface AdminClass extends Base, TimeStamps {}
|
|||||||
@dbconn()
|
@dbconn()
|
||||||
@index({ username: 1 }, { unique: true })
|
@index({ username: 1 }, { unique: true })
|
||||||
@modelOptions({ schemaOptions: { collection: 'admin_user', timestamps: true } })
|
@modelOptions({ schemaOptions: { collection: 'admin_user', timestamps: true } })
|
||||||
|
@pre<AdminClass>('save', function () {
|
||||||
|
this.newRecord = false
|
||||||
|
})
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
class AdminClass extends BaseModule {
|
class AdminClass extends BaseModule {
|
||||||
@prop({ maxlength: 20, minlength: 5, required: true })
|
@prop({ maxlength: 20, minlength: 5, required: true })
|
||||||
@ -86,6 +89,12 @@ class AdminClass extends BaseModule {
|
|||||||
*/
|
*/
|
||||||
@prop()
|
@prop()
|
||||||
public level: number
|
public level: number
|
||||||
|
/**
|
||||||
|
* 是否为新记录, 只用于判断insertOrUpdate返回的记录是否为新纪录
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
@prop({ default: true })
|
||||||
|
public newRecord: boolean
|
||||||
|
|
||||||
public static async findByName(this: ReturnModelType<typeof AdminClass>, username) {
|
public static async findByName(this: ReturnModelType<typeof AdminClass>, username) {
|
||||||
return this.findOne({ username, deleted: false }).exec()
|
return this.findOne({ username, deleted: false }).exec()
|
||||||
|
@ -20,8 +20,10 @@ class GameInfo {
|
|||||||
@dbconn()
|
@dbconn()
|
||||||
@index({ location: '2dsphere' })
|
@index({ location: '2dsphere' })
|
||||||
@index({ sid: 1 }, { unique: true })
|
@index({ sid: 1 }, { unique: true })
|
||||||
|
@index({ partnerId: 1 }, { unique: false })
|
||||||
@modelOptions({ schemaOptions: { collection: 'shop', timestamps: true } })
|
@modelOptions({ schemaOptions: { collection: 'shop', timestamps: true } })
|
||||||
@pre<ShopClass>('save', async function () {
|
@pre<ShopClass>('save', async function () {
|
||||||
|
this.newRecord = false
|
||||||
if (!this.sid) {
|
if (!this.sid) {
|
||||||
let sid = ''
|
let sid = ''
|
||||||
while (!sid) {
|
while (!sid) {
|
||||||
@ -46,6 +48,13 @@ class ShopClass extends BaseModule {
|
|||||||
@prop()
|
@prop()
|
||||||
public numid: number
|
public numid: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从合作伙伴那来的唯一id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@prop()
|
||||||
|
public partnerId: string
|
||||||
|
|
||||||
@prop({ required: true })
|
@prop({ required: true })
|
||||||
public name!: string
|
public name!: string
|
||||||
/**
|
/**
|
||||||
@ -128,6 +137,18 @@ class ShopClass extends BaseModule {
|
|||||||
*/
|
*/
|
||||||
@prop()
|
@prop()
|
||||||
public logo: string
|
public logo: string
|
||||||
|
/**
|
||||||
|
* 合作伙伴那来的logo原始信息
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@prop()
|
||||||
|
public logoPartner: string
|
||||||
|
/**
|
||||||
|
* 资料来源
|
||||||
|
* @type {number} 0: 后台添加, 1: 合作伙伴接口添加
|
||||||
|
*/
|
||||||
|
@prop({ default: 0 })
|
||||||
|
public source: number
|
||||||
/**
|
/**
|
||||||
* 店铺的行业分类
|
* 店铺的行业分类
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -153,6 +174,13 @@ class ShopClass extends BaseModule {
|
|||||||
@prop({ type: () => [String] })
|
@prop({ type: () => [String] })
|
||||||
public qtypes: string[]
|
public qtypes: string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为新记录, 只用于判断insertOrUpdate返回的记录是否为新纪录
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
@prop({ default: true })
|
||||||
|
public newRecord: boolean
|
||||||
|
|
||||||
public static parseQueryParam(params) {
|
public static parseQueryParam(params) {
|
||||||
let options: any = {
|
let options: any = {
|
||||||
opt: { deleted: false, show: true, publish: true },
|
opt: { deleted: false, show: true, publish: true },
|
||||||
|
@ -3,7 +3,13 @@ import * as jetpack from 'fs-jetpack'
|
|||||||
import { Game } from '../models/content/Game'
|
import { Game } from '../models/content/Game'
|
||||||
import { ZError } from '../common/ZError'
|
import { ZError } from '../common/ZError'
|
||||||
import { generateQr } from './Wechat'
|
import { generateQr } from './Wechat'
|
||||||
import nodeHtmlToImage, { NodeHtmlToImageOptions } from 'node-html-to-image'
|
import fs from 'fs'
|
||||||
|
import os from 'os'
|
||||||
|
import nodeHtmlToImage from 'node-html-to-image'
|
||||||
|
import axios from 'axios'
|
||||||
|
import * as http from 'http'
|
||||||
|
import { customAlphabet } from 'nanoid'
|
||||||
|
const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 26)
|
||||||
|
|
||||||
export function generateUploadPath(subPath: string) {
|
export function generateUploadPath(subPath: string) {
|
||||||
const base = config.file.upload_location
|
const base = config.file.upload_location
|
||||||
@ -110,3 +116,28 @@ export function getCouponUrl(shop: string, couponId: string) {
|
|||||||
let subPath = `/coupon/${shop}`
|
let subPath = `/coupon/${shop}`
|
||||||
return `${config.file.show_url}${subPath}/${couponId}.png`
|
return `${config.file.show_url}${subPath}/${couponId}.png`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadRemoteFile(url: string) {
|
||||||
|
const tempPath = os.tmpdir()
|
||||||
|
const fileName = nanoid()
|
||||||
|
const dest: string = `${tempPath}/${fileName}.png`
|
||||||
|
if (jetpack.exists(dest)) {
|
||||||
|
jetpack.remove(dest)
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios({
|
||||||
|
url,
|
||||||
|
responseType: 'stream',
|
||||||
|
}).then(resp => {
|
||||||
|
const writer = fs.createWriteStream(dest)
|
||||||
|
resp.data.pipe(writer)
|
||||||
|
writer.on('finish', () => {
|
||||||
|
console.log('finish')
|
||||||
|
resolve && resolve(dest)
|
||||||
|
})
|
||||||
|
writer.on('error', err => {
|
||||||
|
reject && reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
21
src/services/TencentCDN.ts
Normal file
21
src/services/TencentCDN.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import fs from 'fs'
|
||||||
|
import FormData from 'form-data'
|
||||||
|
|
||||||
|
export function uploadToCDN(localPath: string, subPath: string) {
|
||||||
|
let data = new FormData()
|
||||||
|
data.append('sub_path', subPath)
|
||||||
|
data.append('type', 'image')
|
||||||
|
data.append('image-file', fs.createReadStream(localPath))
|
||||||
|
|
||||||
|
let config: any = {
|
||||||
|
method: 'post',
|
||||||
|
url: 'https://opm.kingsome.cn/api/upload',
|
||||||
|
headers: {
|
||||||
|
...data.getHeaders(),
|
||||||
|
},
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios(config)
|
||||||
|
}
|
20
src/utils/pinyin.ts
Normal file
20
src/utils/pinyin.ts
Normal file
File diff suppressed because one or more lines are too long
@ -36,3 +36,33 @@ export function md5(str) {
|
|||||||
str = md5sum.digest('hex')
|
str = md5sum.digest('hex')
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createSign(secretKey, paramStr, timestamp) {
|
||||||
|
paramStr = `${paramStr}:${timestamp}:${secretKey}`
|
||||||
|
return sha1(paramStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkSign({
|
||||||
|
secretKey,
|
||||||
|
data,
|
||||||
|
timestamp,
|
||||||
|
sign,
|
||||||
|
signKeys,
|
||||||
|
}: {
|
||||||
|
secretKey: string
|
||||||
|
data: {}
|
||||||
|
timestamp: string
|
||||||
|
sign: string
|
||||||
|
signKeys: string[]
|
||||||
|
}) {
|
||||||
|
signKeys.sort()
|
||||||
|
let signStr = ''
|
||||||
|
for (let key of signKeys) {
|
||||||
|
if (signStr.length > 0) {
|
||||||
|
signStr += '&'
|
||||||
|
}
|
||||||
|
signStr += `${key}=${data[key]}`
|
||||||
|
}
|
||||||
|
let sign1 = createSign(secretKey, signStr, timestamp)
|
||||||
|
return sign1 === sign
|
||||||
|
}
|
||||||
|
@ -68,3 +68,39 @@ export function isObjectId(id: string): boolean {
|
|||||||
//mongoose.Types.ObjectId.isValid(id)
|
//mongoose.Types.ObjectId.isValid(id)
|
||||||
return /^[a-fA-F0-9]{24}$/.test(id)
|
return /^[a-fA-F0-9]{24}$/.test(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 10进制 -> 62进制
|
||||||
|
* @param {string | number} number
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export function string10to62(number: string | number) {
|
||||||
|
const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split('')
|
||||||
|
const radix = chars.length
|
||||||
|
let qutient = +number
|
||||||
|
const arr = []
|
||||||
|
do {
|
||||||
|
const mod = qutient % radix
|
||||||
|
qutient = (qutient - mod) / radix
|
||||||
|
arr.unshift(chars[mod])
|
||||||
|
} while (qutient)
|
||||||
|
return arr.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 62进制 -> 10 进制
|
||||||
|
* @param {string} numberCode
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
export function string62to10(numberCode: string) {
|
||||||
|
const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'
|
||||||
|
const radix = chars.length
|
||||||
|
numberCode = numberCode + ''
|
||||||
|
const len = numberCode.length
|
||||||
|
let i = 0
|
||||||
|
let originNumber = 0
|
||||||
|
while (i < len) {
|
||||||
|
originNumber += Math.pow(radix, i++) * (chars.indexOf(numberCode.charAt(len - i)) || 0)
|
||||||
|
}
|
||||||
|
return originNumber
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user